From fe9d6f34ff419f8dfcf8452663ac2fe0a6e69102 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:46:34 +0100 Subject: [PATCH] `[REQUIRES_FULL_CACHE_FLUSH_WHEN_RELEASED]` Refactor `FlatEntity` to be `UniversalFlatEntity` superset (#17452) # Introduction In this PR we're refactoring the `FlatEntity` type to become a superset of the `UniversalFlatEntity`. Right now we're storing all the extra properties in `__universal` property, at some point it might just be sibling to other entity and we might rely on the `propertiesToCompare` constants and TypeScript allowing passing a superset type into a smaller subset type ## FromTo utils The entity to flat entity method now computes the universal information, standardized a typing and pattern to do ## Example Also strictly type ```ts "bbb019ea-6205-498c-aea5-67bc53bce8a9": { "workspaceId": "20202020-1c25-4d02-bf25-6aeccf7ea419", "universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002", "applicationId": "d01b010d-b984-465b-b40b-370e954e5188", "id": "bbb019ea-6205-498c-aea5-67bc53bce8a9", "pageLayoutTabId": "791a512f-169f-4209-b731-aa86716668c6", "title": "Deals by Company", "type": "GRAPH", "objectMetadataId": "9e14efea-df5b-4c0e-aba9-cfe455f32397", "gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 }, "configuration": { "color": "orange", "orderBy": "FIELD_ASC", "timezone": "UTC", "displayLegend": true, "displayDataLabel": false, "showCenterMetric": true, "configurationType": "PIE_CHART", "firstDayOfTheWeek": 0, "aggregateOperation": "COUNT", "groupBySubFieldName": "name", "groupByFieldMetadataId": "6673ff18-63d2-47a1-8f85-2b9b09ca27a5", "aggregateFieldMetadataId": "8d64ee41-5dd4-4de6-945a-7c0c18399715" }, "createdAt": "2026-01-28T14:08:52.140Z", "updatedAt": "2026-01-28T14:08:52.140Z", "deletedAt": null, "__universal": { "universalIdentifier": "20202020-d111-4d11-8d11-da5ab0a11002", "applicationUniversalIdentifier": "20202020-64aa-4b6f-b003-9c74b97cee20", "pageLayoutTabUniversalIdentifier": "20202020-d011-4d11-8d11-da5ab0a01001", "objectMetadataUniversalIdentifier": "20202020-9549-49dd-b2b2-883999db8938", "gridPosition": { "row": 0, "column": 6, "rowSpan": 6, "columnSpan": 6 }, "configuration": { "color": "orange", "orderBy": "FIELD_ASC", "timezone": "UTC", "displayLegend": true, "displayDataLabel": false, "showCenterMetric": true, "configurationType": "PIE_CHART", "firstDayOfTheWeek": 0, "aggregateOperation": "COUNT", "groupBySubFieldName": "name", "aggregateFieldMetadataUniversalIdentifier": "20202020-d01a-4131-8a31-f123456789ab", "groupByFieldMetadataUniversalIdentifier": "20202020-cbac-457e-b565-adece5fc815f" } } }, ``` --- ...is-field-metadata-settings-of-type.util.ts | 25 +++ .../flat-agent/flat-agent.module.ts | 9 +- .../workspace-flat-agent-map-cache.service.ts | 27 ++- ...ce-flat-role-target-by-agent-id.service.ts | 47 +++-- ...ansform-agent-entity-to-flat-agent.util.ts | 31 +++- .../flat-command-menu-item.module.ts | 8 +- ...lat-command-menu-item-map-cache.service.ts | 40 ++++- ...m-entity-to-flat-command-menu-item.util.ts | 49 +++++- .../all-metadata-relations.constant.ts | 17 +- ...ny-or-all-flat-entity-maps-cache.module.ts | 4 + ...x-to-entity-many-to-one-properties.type.ts | 13 ++ ...x-to-entity-one-to-many-properties.type.ts | 14 ++ .../types/flat-entity-from.type.ts | 50 ++++-- .../__mocks__/get-flat-field-metadata.mock.ts | 2 + ...elation-target-flat-field-metadata-mock.ts | 1 + ...e-flat-field-metadata-map-cache.service.ts | 65 +++++-- .../types/flat-field-metadata.type.ts | 3 +- ...data-entity-to-flat-field-metadata.util.ts | 123 ++++++++++++- .../flat-front-component.module.ts | 3 +- ...-flat-front-component-map-cache.service.ts | 26 ++- ...ent-entity-to-flat-front-component.util.ts | 31 +++- .../workspace-flat-index-map-cache.service.ts | 53 ++++-- ...data-entity-to-flat-index-metadata.util.ts | 45 ++++- .../flat-navigation-menu-item.module.ts | 10 +- ...-navigation-menu-item-map-cache.service.ts | 55 +++++- ...ntity-to-flat-navigation-menu-item.util.ts | 84 ++++++++- ...-flat-object-metadata-map-cache.service.ts | 71 +++++--- .../types/flat-object-metadata.type.ts | 9 +- ...ata-entity-to-flat-object-metadata.util.ts | 92 ++++++++-- .../flat-page-layout-tab.module.ts | 9 +- ...-flat-page-layout-tab-map-cache.service.ts | 58 ++++-- ...tab-entity-to-flat-page-layout-tab.util.ts | 46 ++++- .../flat-page-layout-widget.module.ts | 14 +- ...at-page-layout-widget-map-cache.service.ts | 69 +++++++- ...uration-to-universal-configuration.util.ts | 166 ++++++++++++++++++ ...-entity-to-flat-page-layout-widget.util.ts | 78 +++++++- .../flat-page-layout.module.ts | 11 +- ...pace-flat-page-layout-map-cache.service.ts | 54 ++++-- ...-layout-entity-to-flat-page-layout.util.ts | 50 +++++- ...pace-flat-role-target-map-cache.service.ts | 43 ++++- ...-target-entity-to-flat-role-target.util.ts | 62 +++++-- .../from-role-entity-to-flat-role.util.ts | 106 ++++++++--- ...ssion-predicate-group-map-cache.service.ts | 98 ++++++++++- ...-permission-predicate-map-cache.service.ts | 81 ++++++++- ...lat-row-level-permission-predicate.util.ts | 118 ++++++++++++- ...w-level-permission-predicate-group.util.ts | 101 ++++++++++- .../flat-skill/flat-skill.module.ts | 3 +- .../workspace-flat-skill-map-cache.service.ts | 27 ++- .../from-skill-entity-to-flat-skill.util.ts | 29 ++- ...space-flat-view-field-map-cache.service.ts | 55 +++++- ...ew-field-entity-to-flat-view-field.util.ts | 56 +++++- ...lat-view-filter-group-map-cache.service.ts | 66 +++++-- ...p-entity-to-flat-view-filter-group.util.ts | 83 +++++++-- ...pace-flat-view-filter-map-cache.service.ts | 64 ++++++- ...-filter-entity-to-flat-view-filter.util.ts | 75 +++++++- ...space-flat-view-group-map-cache.service.ts | 42 ++++- ...ew-group-entity-to-flat-view-group.util.ts | 42 ++++- .../flat-view/flat-view.module.ts | 6 + .../workspace-flat-view-map-cache.service.ts | 111 ++++++++---- .../from-view-entity-to-flat-view.util.ts | 116 ++++++++++-- .../flat-webhook/flat-webhook.module.ts | 3 +- ...orkspace-flat-webhook-map-cache.service.ts | 24 ++- ...rom-webhook-entity-to-flat-webhook.util.ts | 29 ++- .../logic-function/logic-function.module.ts | 7 +- ...e-flat-logic-function-map-cache.service.ts | 27 ++- ...tion-entity-to-flat-logic-function.util.ts | 41 ++++- .../object-metadata/object-metadata.entity.ts | 3 +- .../metadata-modules/role/role.module.ts | 2 + .../workspace-flat-role-map-cache.service.ts | 42 +++-- .../webhook/webhook.module.ts | 3 +- .../webhook/webhook.service.ts | 50 +++++- ...ne-id-by-universal-identifier-maps.type.ts | 14 ++ ...th-regrouped-one-to-many-relations.type.ts | 15 ++ .../from-entity-to-flat-entity-args.type.ts | 9 + ...ate-id-to-universal-identifier-map.util.ts | 11 ++ .../regroup-entities-by-related-entity-id.ts | 11 +- ...nb-properties-by-metadata-name.constant.ts | 56 ++++++ ...erialized-relation-properties.test-type.ts | 117 ++++++++++-- ...universal-flat-field-metadata.test-type.ts | 10 +- ...ord-serialized-relation-properties.type.ts | 33 +++- .../types/universal-flat-entity-from.type.ts | 50 +++--- .../types/universal-flat-role.type.ts | 4 + ...pdated-matrix-dispatcher.util.spec.ts.snap | 6 +- .../types/workspace-migration-field-action.ts | 1 + ...ected-account-delete-one.pre-query.hook.ts | 28 ++- .../connected-account-query-hook.module.ts | 6 +- ...xtractSerializedRelationProperties.type.ts | 7 +- .../src/types/IsSerializedRelation.type.ts | 4 + ...erialized-relation-properties.type-test.ts | 16 ++ packages/twenty-shared/src/types/index.ts | 1 + 90 files changed, 3048 insertions(+), 528 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type.ts create mode 100644 packages/twenty-shared/src/types/IsSerializedRelation.type.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util.ts new file mode 100644 index 00000000000..2cb7c8e16c1 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util.ts @@ -0,0 +1,25 @@ +import { + type AllFieldMetadataSettings, + type FieldMetadataSettingsMapping, + FieldMetadataType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +export const isFieldMetadataSettingsOfType = < + T extends keyof FieldMetadataSettingsMapping, +>( + settings: AllFieldMetadataSettings | null, + fieldMetadataType: T, +): settings is FieldMetadataSettingsMapping[T] => { + // Settings don't have a discriminator - the type is determined by fieldMetadataType + // For required settings types (RELATION, MORPH_RELATION, FILES), ensure settings is defined + if ( + fieldMetadataType === FieldMetadataType.RELATION || + fieldMetadataType === FieldMetadataType.MORPH_RELATION || + fieldMetadataType === FieldMetadataType.FILES + ) { + return isDefined(settings); + } + + return true; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/flat-agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/flat-agent.module.ts index 354b990ebcd..67ce9b5bf61 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/flat-agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/flat-agent.module.ts @@ -1,15 +1,22 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { WorkspaceFlatAgentMapCacheService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service'; import { WorkspaceFlatRoleTargetByAgentIdService } from 'src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([AgentEntity, RoleTargetEntity]), + TypeOrmModule.forFeature([ + AgentEntity, + ApplicationEntity, + RoleEntity, + RoleTargetEntity, + ]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service.ts index e991367c597..e8133800fd4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-agent-map-cache.service.ts @@ -5,11 +5,13 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type'; import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,20 +20,35 @@ export class WorkspaceFlatAgentMapCacheService extends WorkspaceCacheProvider, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const agents = await this.agentRepository.find({ - where: { workspaceId }, - withDeleted: true, - }); + const [agents, applications] = await Promise.all([ + this.agentRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); const flatAgentMaps = createEmptyFlatEntityMaps(); for (const agentEntity of agents) { - const flatAgent = transformAgentEntityToFlatAgent(agentEntity); + const flatAgent = transformAgentEntityToFlatAgent({ + entity: agentEntity, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatAgent, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service.ts index d44a6b089ce..530aa23f2ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/services/workspace-flat-role-target-by-agent-id.service.ts @@ -6,10 +6,13 @@ import { IsNull, Not, Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type'; -import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; +import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; @Injectable() @WorkspaceCache('flatRoleTargetByAgentIdMaps') @@ -17,6 +20,10 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi constructor( @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, ) { super(); } @@ -24,13 +31,30 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi async computeForCache( workspaceId: string, ): Promise { - const roleTargetEntities = await this.roleTargetRepository.find({ - where: { - workspaceId, - agentId: Not(IsNull()), - }, - withDeleted: true, - }); + const [roleTargetEntities, applications, roles] = await Promise.all([ + this.roleTargetRepository.find({ + where: { + workspaceId, + agentId: Not(IsNull()), + }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.roleRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); const flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps = {}; @@ -38,8 +62,11 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi Omit & NonNullableRequired> >) { - const flatRoleTarget = - fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity); + const flatRoleTarget = fromRoleTargetEntityToFlatRoleTarget({ + entity: roleTargetEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + }); flatRoleTargetByAgentIdMaps[roleTargetEntity.agentId] = flatRoleTarget; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts index 142c33fa0cb..c53f805fc7d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts @@ -1,9 +1,26 @@ -import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; +import { isDefined } from 'twenty-shared/utils'; + import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const transformAgentEntityToFlatAgent = ({ + entity: agentEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'agent'>): FlatAgent => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(agentEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${agentEntity.applicationId} not found for agent ${agentEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const transformAgentEntityToFlatAgent = ( - agentEntity: AgentEntity, -): FlatAgent => { return { createdAt: agentEntity.createdAt.toISOString(), deletedAt: agentEntity.deletedAt?.toISOString() ?? null, @@ -23,5 +40,11 @@ export const transformAgentEntityToFlatAgent = ( applicationId: agentEntity.applicationId, modelConfiguration: agentEntity.modelConfiguration, evaluationInputs: agentEntity.evaluationInputs, + __universal: { + universalIdentifier: agentEntity.universalIdentifier, + applicationUniversalIdentifier, + responseFormat: agentEntity.responseFormat, + modelConfiguration: agentEntity.modelConfiguration, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module.ts index 7ef73edcc12..930bf58799d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module.ts @@ -1,13 +1,19 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity'; import { WorkspaceFlatCommandMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([CommandMenuItemEntity]), + TypeOrmModule.forFeature([ + CommandMenuItemEntity, + ApplicationEntity, + ObjectMetadataEntity, + ]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatCommandMenuItemMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service.ts index 336deb7ea0f..0046d1e0181 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service.ts @@ -5,11 +5,14 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity'; import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type'; import { fromCommandMenuItemEntityToFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,21 +21,48 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP constructor( @InjectRepository(CommandMenuItemEntity) private readonly commandMenuItemRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const commandMenuItems = await this.commandMenuItemRepository.find({ - where: { workspaceId }, - withDeleted: true, - }); + const [commandMenuItems, applications, objectMetadatas] = await Promise.all( + [ + this.commandMenuItemRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ], + ); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); const flatCommandMenuItemMaps = createEmptyFlatEntityMaps(); for (const commandMenuItemEntity of commandMenuItems) { const flatCommandMenuItem = - fromCommandMenuItemEntityToFlatCommandMenuItem(commandMenuItemEntity); + fromCommandMenuItemEntityToFlatCommandMenuItem({ + entity: commandMenuItemEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatCommandMenuItem, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util.ts index 429fdc1bc3d..a304fcf1d85 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util.ts @@ -1,9 +1,45 @@ -import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity'; +import { isDefined } from 'twenty-shared/utils'; + import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({ + entity: commandMenuItemEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'commandMenuItem'>): FlatCommandMenuItem => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + commandMenuItemEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${commandMenuItemEntity.applicationId} not found for commandMenuItem ${commandMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let availabilityObjectMetadataUniversalIdentifier: string | null = null; + + if (isDefined(commandMenuItemEntity.availabilityObjectMetadataId)) { + availabilityObjectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + commandMenuItemEntity.availabilityObjectMetadataId, + ) ?? null; + + if (!isDefined(availabilityObjectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${commandMenuItemEntity.availabilityObjectMetadataId} not found for commandMenuItem ${commandMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } -export const fromCommandMenuItemEntityToFlatCommandMenuItem = ( - commandMenuItemEntity: CommandMenuItemEntity, -): FlatCommandMenuItem => { return { id: commandMenuItemEntity.id, workflowVersionId: commandMenuItemEntity.workflowVersionId, @@ -18,5 +54,10 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ( applicationId: commandMenuItemEntity.applicationId, createdAt: commandMenuItemEntity.createdAt.toISOString(), updatedAt: commandMenuItemEntity.updatedAt.toISOString(), + __universal: { + universalIdentifier: commandMenuItemEntity.universalIdentifier, + applicationUniversalIdentifier, + availabilityObjectMetadataUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts index 17c5f3ea6dc..e9da7d71a35 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant.ts @@ -1,16 +1,13 @@ 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 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 MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; type ManyToOneRelationValue< @@ -24,12 +21,12 @@ type ManyToOneRelationValue< > extends Relation ? { metadataName: FromMetadataEntityToMetadataName; - flatEntityForeignKeyAggregator: ExtractPropertiesThatEndsWithIds< - MetadataFlatEntity> - // Note: In the best of the world should not be nullable, entities should always declare inverside keys - > | null; + flatEntityForeignKeyAggregator: + | keyof AddSuffixToEntityOneToManyProperties + | null; + // Note: In the best of the world should not be nullable, entities should always declare inverside keys foreignKey: ExtractPropertiesThatEndsWithId< - MetadataFlatEntity, + MetadataEntity, 'id' | 'workspaceId' >; } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index be2fbc436c6..52e4120ea6f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service'; @@ -22,6 +23,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; @@ -49,6 +51,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache PageLayoutWidgetEntity, RowLevelPermissionPredicateEntity, RowLevelPermissionPredicateGroupEntity, + ApplicationEntity, + RoleEntity, ]), ], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts new file mode 100644 index 00000000000..3da8d53c33c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type.ts @@ -0,0 +1,13 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; +import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; + +export type AddSuffixToEntityManyToOneProperties< + TEntity, + TMetadataName extends AllMetadataName, + TSuffix extends string, +> = { + [P in Extract, keyof TEntity> & + string as `${RemoveSuffix}${Capitalize}`]: TEntity[P]; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts new file mode 100644 index 00000000000..1e1046551f6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type.ts @@ -0,0 +1,14 @@ +import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type'; +import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; + +export type AddSuffixToEntityOneToManyProperties< + TEntity, + TSuffix extends string, +> = { + [P in ExtractEntityOneToManyEntityRelationProperties< + TEntity, + SyncableEntity + > & + string as `${RemoveSuffix}${Capitalize}`]: string[]; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-from.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-from.type.ts index d5590074fcd..a456f32a9f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-from.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-from.type.ts @@ -1,26 +1,42 @@ -import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type'; -import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type'; -import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type'; -import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; -import { type NonNullableProperties } from 'src/types/non-nullable-properties.type'; +import { type AllMetadataName } from 'twenty-shared/metadata'; -export type SyncableFlatEntity = NonNullableProperties< - Omit +import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type'; +import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type'; +import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type'; +import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type'; +import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { type UniversalFlatEntityExtraProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type SyncableFlatEntity = Omit< + SyncableEntity, + 'application' | 'workspace' > & { id: string; - applicationId: string | null; }; -export type FlatEntityFrom = Omit< +export type FlatEntityFrom< + TEntity, + TMetadataName extends AllMetadataName | undefined = undefined, +> = Omit< TEntity, | ExtractEntityRelatedEntityProperties | keyof CastRecordTypeOrmDatePropertiesToString > & - CastRecordTypeOrmDatePropertiesToString & { - [P in ExtractEntityOneToManyEntityRelationProperties< - TEntity, - SyncableEntity - > & - string as `${RemoveSuffix}Ids`]: string[]; - }; + CastRecordTypeOrmDatePropertiesToString & + AddSuffixToEntityOneToManyProperties & + (TEntity extends SyncableEntity + ? { + /** + * /!\ Under migration the idea is at some point to replace FlatEntity by UniversalFlatEntity /!\ + * Please avoid any usage or contact me ( prastoin ) before doing so + * TODO remove with FlatEntity once it has been fully migrated + */ + __universal?: UniversalFlatEntityExtraProperties< + TEntity, + TMetadataName extends undefined + ? FromMetadataEntityToMetadataName + : TMetadataName + > & { universalIdentifier: string }; + } + : // eslint-disable-next-line @typescript-eslint/no-empty-object-type + {}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts index 3236b8736cb..0b26f0d3a76 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts @@ -45,6 +45,8 @@ export const getFlatFieldMetadataMock = ( applicationId: faker.string.uuid(), relationTargetFieldMetadataId: null, relationTargetObjectMetadataId: null, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + __universal: {} as any, ...overrides, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts index 89689f22ae6..73316193836 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts @@ -65,5 +65,6 @@ export const getRelationTargetFlatFieldMetadataMock = ({ defaultValue: null, options: null, applicationId: faker.string.uuid(), + __universal: undefined, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts index 7004c04ce59..fb2b1d22d73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts @@ -5,16 +5,19 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; 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 { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -26,6 +29,10 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro constructor( @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, @InjectRepository(ViewFieldEntity) private readonly viewFieldRepository: Repository, @InjectRepository(ViewFilterEntity) @@ -41,25 +48,43 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro async computeForCache( workspaceId: string, ): Promise> { - const [fieldMetadatas, viewFields, viewFilters, views] = await Promise.all([ + const [ + fieldMetadatas, + objectMetadatas, + applications, + viewFields, + viewFilters, + views, + ] = await Promise.all([ this.fieldMetadataRepository.find({ where: { workspaceId }, withDeleted: true, }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), this.viewFieldRepository.find({ where: { workspaceId }, - select: ['id', 'fieldMetadataId'], + select: ['id', 'universalIdentifier', 'fieldMetadataId'], withDeleted: true, }), this.viewFilterRepository.find({ where: { workspaceId }, - select: ['id', 'fieldMetadataId'], + select: ['id', 'universalIdentifier', 'fieldMetadataId'], withDeleted: true, }), this.viewRepository.find({ where: { workspaceId }, select: [ 'id', + 'universalIdentifier', 'kanbanAggregateOperationFieldMetadataId', 'calendarFieldMetadataId', 'mainGroupByFieldMetadataId', @@ -99,20 +124,34 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCachePro ] as const ).map(regroupEntitiesByRelatedEntityId); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const flatFieldMetadataMaps = createEmptyFlatEntityMaps(); for (const fieldMetadataEntity of fieldMetadatas) { const flatFieldMetadata = fromFieldMetadataEntityToFlatFieldMetadata({ - ...fieldMetadataEntity, - viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [], - viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [], - kanbanAggregateOperationViews: - kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [], - calendarViews: calendarViewsByFieldId.get(fieldMetadataEntity.id) || [], - mainGroupByFieldMetadataViews: - mainGroupByFieldMetadataViewsByFieldId.get(fieldMetadataEntity.id) || - [], - } as FieldMetadataEntity); + entity: { + ...fieldMetadataEntity, + viewFields: viewFieldsByFieldId.get(fieldMetadataEntity.id) || [], + viewFilters: viewFiltersByFieldId.get(fieldMetadataEntity.id) || [], + kanbanAggregateOperationViews: + kanbanViewsByFieldId.get(fieldMetadataEntity.id) || [], + calendarViews: + calendarViewsByFieldId.get(fieldMetadataEntity.id) || [], + mainGroupByFieldMetadataViews: + mainGroupByFieldMetadataViewsByFieldId.get( + fieldMetadataEntity.id, + ) || [], + }, + fieldMetadataIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatFieldMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts index c52d078682f..680e76aad7c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts @@ -8,5 +8,6 @@ export type FlatFieldMetadata = Omit< FieldMetadataEntity, 'relationTargetFieldMetadata' | 'relationTargetObjectMetadata' - > + >, + 'fieldMetadata' >; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts index 733a34e644d..33b7b683930 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts @@ -1,20 +1,97 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isFieldMetadataSettingsOfType } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromFieldMetadataEntityToFlatFieldMetadata = < - T extends FieldMetadataType, ->( - fieldMetadataEntity: FieldMetadataEntity, - // This is intended to be abstract -): FlatFieldMetadata => { +export const fromFieldMetadataEntityToFlatFieldMetadata = ({ + entity: fieldMetadataEntity, + fieldMetadataIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'fieldMetadata'>): FlatFieldMetadata => { const fieldMetadataWithoutRelations = removePropertiesFromRecord( fieldMetadataEntity, getMetadataEntityRelationProperties('fieldMetadata'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + fieldMetadataEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${fieldMetadataEntity.applicationId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + fieldMetadataEntity.objectMetadataId, + ); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Object metadata with id ${fieldMetadataEntity.objectMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let relationTargetObjectMetadataUniversalIdentifier: string | null = null; + + if (isDefined(fieldMetadataEntity.relationTargetObjectMetadataId)) { + relationTargetObjectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + fieldMetadataEntity.relationTargetObjectMetadataId, + ) ?? null; + + if (!isDefined(relationTargetObjectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Relation target object metadata with id ${fieldMetadataEntity.relationTargetObjectMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let relationTargetFieldMetadataUniversalIdentifier: string | null = null; + + if (isDefined(fieldMetadataEntity.relationTargetFieldMetadataId)) { + relationTargetFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + fieldMetadataEntity.relationTargetFieldMetadataId, + ) ?? null; + + if (!isDefined(relationTargetFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Relation target field metadata with id ${fieldMetadataEntity.relationTargetFieldMetadataId} not found when building flat field metadata for field ${fieldMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + const settings = fieldMetadataEntity.settings; + const isRelationSettings = + isFieldMetadataSettingsOfType(settings, FieldMetadataType.RELATION) || + isFieldMetadataSettingsOfType(settings, FieldMetadataType.MORPH_RELATION); + + const settingsWithUniversalIdentifiers = isRelationSettings + ? { + ...settings, + ...(isDefined(settings.junctionTargetFieldId) && { + junctionTargetFieldUniversalIdentifier: + fieldMetadataIdToUniversalIdentifierMap.get( + settings.junctionTargetFieldId, + ), + }), + } + : settings; return { ...fieldMetadataWithoutRelations, @@ -29,5 +106,33 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = < [], viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id), viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id), + __universal: { + universalIdentifier: fieldMetadataWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + objectMetadataUniversalIdentifier, + relationTargetObjectMetadataUniversalIdentifier, + relationTargetFieldMetadataUniversalIdentifier, + viewFieldUniversalIdentifiers: fieldMetadataEntity.viewFields.map( + ({ universalIdentifier }) => universalIdentifier, + ), + viewFilterUniversalIdentifiers: fieldMetadataEntity.viewFilters.map( + ({ universalIdentifier }) => universalIdentifier, + ), + kanbanAggregateOperationViewUniversalIdentifiers: + fieldMetadataEntity.kanbanAggregateOperationViews.map( + ({ universalIdentifier }) => universalIdentifier, + ), + calendarViewUniversalIdentifiers: fieldMetadataEntity.calendarViews.map( + ({ universalIdentifier }) => universalIdentifier, + ), + mainGroupByFieldMetadataViewUniversalIdentifiers: + fieldMetadataEntity.mainGroupByFieldMetadataViews?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], + settings: settingsWithUniversalIdentifiers, + defaultValue: fieldMetadataWithoutRelations.defaultValue, + options: fieldMetadataWithoutRelations.options, + standardOverrides: fieldMetadataWithoutRelations.standardOverrides, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts index aeaacf17c3b..6363e2052b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/flat-front-component.module.ts @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceFlatFrontComponentMapCacheService } from 'src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service'; import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([FrontComponentEntity]), + TypeOrmModule.forFeature([ApplicationEntity, FrontComponentEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatFrontComponentMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts index 1c64005e601..5cd0c2a20d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/services/workspace-flat-front-component-map-cache.service.ts @@ -5,11 +5,13 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type'; import { fromFrontComponentEntityToFlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util'; import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,20 +20,34 @@ export class WorkspaceFlatFrontComponentMapCacheService extends WorkspaceCachePr constructor( @InjectRepository(FrontComponentEntity) private readonly frontComponentRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const frontComponents = await this.frontComponentRepository.find({ - where: { workspaceId }, - }); + const [frontComponents, applications] = await Promise.all([ + this.frontComponentRepository.find({ + where: { workspaceId }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); const flatFrontComponentMaps = createEmptyFlatEntityMaps(); for (const frontComponentEntity of frontComponents) { - const flatFrontComponent = - fromFrontComponentEntityToFlatFrontComponent(frontComponentEntity); + const flatFrontComponent = fromFrontComponentEntityToFlatFrontComponent({ + entity: frontComponentEntity, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatFrontComponent, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts index bb413e054be..22233637cbe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-front-component/utils/from-front-component-entity-to-flat-front-component.util.ts @@ -1,9 +1,28 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; -import { type FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromFrontComponentEntityToFlatFrontComponent = ({ + entity: frontComponentEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'frontComponent'>): FlatFrontComponent => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + frontComponentEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${frontComponentEntity.applicationId} not found for frontComponent ${frontComponentEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromFrontComponentEntityToFlatFrontComponent = ( - frontComponentEntity: FrontComponentEntity, -): FlatFrontComponent => { return { id: frontComponentEntity.id, name: frontComponentEntity.name, @@ -12,5 +31,9 @@ export const fromFrontComponentEntityToFlatFrontComponent = ( applicationId: frontComponentEntity.applicationId, createdAt: frontComponentEntity.createdAt.toISOString(), updatedAt: frontComponentEntity.updatedAt.toISOString(), + __universal: { + universalIdentifier: frontComponentEntity.universalIdentifier, + applicationUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts index 3090d8dd507..ea627116674 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts @@ -5,12 +5,15 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; 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 { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { fromIndexMetadataEntityToFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -21,6 +24,10 @@ export class WorkspaceFlatIndexMapCacheService extends WorkspaceCacheProvider< constructor( @InjectRepository(IndexMetadataEntity) private readonly indexMetadataRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, ) { super(); } @@ -28,23 +35,43 @@ export class WorkspaceFlatIndexMapCacheService extends WorkspaceCacheProvider< async computeForCache( workspaceId: string, ): Promise> { - const indexes = await this.indexMetadataRepository.find({ - where: { - workspaceId, - }, - withDeleted: true, - relationLoadStrategy: 'join', - select: { - // Note: We need all IndexFieldMetadataEntity in order to build a FlatIndex - indexFieldMetadatas: true, - }, - relations: ['indexFieldMetadatas'], - }); + const [indexes, applications, objectMetadatas] = await Promise.all([ + this.indexMetadataRepository.find({ + where: { + workspaceId, + }, + withDeleted: true, + relationLoadStrategy: 'join', + select: { + // Note: We need all IndexFieldMetadataEntity in order to build a FlatIndex + indexFieldMetadatas: true, + }, + relations: ['indexFieldMetadatas'], + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); const flatIndexMaps = createEmptyFlatEntityMaps(); for (const indexEntity of indexes) { - const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata(indexEntity); + const flatIndex = fromIndexMetadataEntityToFlatIndexMetadata({ + entity: indexEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatIndex, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util.ts index db87999c011..19582dc909b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util.ts @@ -1,12 +1,42 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromIndexMetadataEntityToFlatIndexMetadata = ({ + entity: indexMetadataEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'index'>): FlatIndexMetadata => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + indexMetadataEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${indexMetadataEntity.applicationId} not found for index ${indexMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + indexMetadataEntity.objectMetadataId, + ); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${indexMetadataEntity.objectMetadataId} not found for index ${indexMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromIndexMetadataEntityToFlatIndexMetadata = ( - indexMetadataEntity: IndexMetadataEntity, -): FlatIndexMetadata => { const indexMetadataEntityWithoutRelations = removePropertiesFromRecord( indexMetadataEntity, getMetadataEntityRelationProperties('index'), @@ -28,5 +58,10 @@ export const fromIndexMetadataEntityToFlatIndexMetadata = ( updatedAt: indexFieldMetadata.updatedAt.toISOString(), }), ), + __universal: { + universalIdentifier: indexMetadataEntity.universalIdentifier, + applicationUniversalIdentifier, + objectMetadataUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module.ts index 0c7450e14d4..c5e2a6a3579 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/flat-navigation-menu-item.module.ts @@ -1,13 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceFlatNavigationMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service'; import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([NavigationMenuItemEntity]), + TypeOrmModule.forFeature([ + NavigationMenuItemEntity, + ApplicationEntity, + ObjectMetadataEntity, + ViewEntity, + ]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatNavigationMenuItemMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service.ts index 4996f8b43c0..1a923b0613d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/services/workspace-flat-navigation-menu-item-map-cache.service.ts @@ -5,12 +5,16 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type'; import { addFlatNavigationMenuItemToMapsAndUpdateIndex } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/add-flat-navigation-menu-item-to-maps-and-update-index.util'; import { fromNavigationMenuItemEntityToFlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util'; import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; @Injectable() @WorkspaceCache('flatNavigationMenuItemMaps') @@ -18,6 +22,12 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac constructor( @InjectRepository(NavigationMenuItemEntity) private readonly navigationMenuItemRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } @@ -25,10 +35,37 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac async computeForCache( workspaceId: string, ): Promise { - const navigationMenuItems = await this.navigationMenuItemRepository.find({ - where: { workspaceId }, - withDeleted: true, - }); + const [navigationMenuItems, applications, objectMetadatas, views] = + await Promise.all([ + this.navigationMenuItemRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const navigationMenuItemIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(navigationMenuItems); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); const flatNavigationMenuItemMaps = { ...createEmptyFlatEntityMaps(), @@ -37,9 +74,13 @@ export class WorkspaceFlatNavigationMenuItemMapCacheService extends WorkspaceCac for (const navigationMenuItemEntity of navigationMenuItems) { const flatNavigationMenuItem = - fromNavigationMenuItemEntityToFlatNavigationMenuItem( - navigationMenuItemEntity, - ); + fromNavigationMenuItemEntityToFlatNavigationMenuItem({ + entity: navigationMenuItemEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + navigationMenuItemIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); addFlatNavigationMenuItemToMapsAndUpdateIndex({ flatNavigationMenuItem, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util.ts index 5a3f5f3a8d4..98c18f2d0e4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-navigation-menu-item/utils/from-navigation-menu-item-entity-to-flat-navigation-menu-item.util.ts @@ -1,9 +1,78 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type'; -import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({ + entity: navigationMenuItemEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + navigationMenuItemIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'navigationMenuItem'>): FlatNavigationMenuItem => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + navigationMenuItemEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${navigationMenuItemEntity.applicationId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let targetObjectMetadataUniversalIdentifier: string | null = null; + + if (isDefined(navigationMenuItemEntity.targetObjectMetadataId)) { + targetObjectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + navigationMenuItemEntity.targetObjectMetadataId, + ) ?? null; + + if (!isDefined(targetObjectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${navigationMenuItemEntity.targetObjectMetadataId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let folderUniversalIdentifier: string | null = null; + + if (isDefined(navigationMenuItemEntity.folderId)) { + folderUniversalIdentifier = + navigationMenuItemIdToUniversalIdentifierMap.get( + navigationMenuItemEntity.folderId, + ) ?? null; + + if (!isDefined(folderUniversalIdentifier)) { + throw new FlatEntityMapsException( + `NavigationMenuItem (folder) with id ${navigationMenuItemEntity.folderId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let viewUniversalIdentifier: string | null = null; + + if (isDefined(navigationMenuItemEntity.viewId)) { + viewUniversalIdentifier = + viewIdToUniversalIdentifierMap.get(navigationMenuItemEntity.viewId) ?? + null; + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${navigationMenuItemEntity.viewId} not found for navigationMenuItem ${navigationMenuItemEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } -export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ( - navigationMenuItemEntity: NavigationMenuItemEntity, -): FlatNavigationMenuItem => { return { id: navigationMenuItemEntity.id, userWorkspaceId: navigationMenuItemEntity.userWorkspaceId, @@ -18,5 +87,12 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ( applicationId: navigationMenuItemEntity.applicationId, createdAt: navigationMenuItemEntity.createdAt.toISOString(), updatedAt: navigationMenuItemEntity.updatedAt.toISOString(), + __universal: { + universalIdentifier: navigationMenuItemEntity.universalIdentifier, + applicationUniversalIdentifier, + targetObjectMetadataUniversalIdentifier, + folderUniversalIdentifier, + viewUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts index 3ad49bc5f5e..13ff1f38369 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; 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'; @@ -14,6 +15,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -25,6 +27,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr constructor( @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, @InjectRepository(IndexMetadataEntity) @@ -38,27 +42,33 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr async computeForCache( workspaceId: string, ): Promise> { - const [objectMetadatas, fields, indexMetadatas, views] = await Promise.all([ - this.objectMetadataRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - this.fieldMetadataRepository.find({ - where: { workspaceId }, - select: ['id', 'objectMetadataId'], - withDeleted: true, - }), - this.indexMetadataRepository.find({ - where: { workspaceId }, - select: ['id', 'objectMetadataId'], - withDeleted: true, - }), - this.viewRepository.find({ - where: { workspaceId }, - select: ['id', 'objectMetadataId'], - withDeleted: true, - }), - ]); + const [objectMetadatas, applications, fields, indexMetadatas, views] = + await Promise.all([ + this.objectMetadataRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + this.indexMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'objectMetadataId'], + withDeleted: true, + }), + ]); const [fieldsByObjectId, indexesByObjectId, viewsByObjectId] = ( [ @@ -77,15 +87,24 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr ] as const ).map(regroupEntitiesByRelatedEntityId); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fields); + const flatObjectMetadataMaps = createEmptyFlatEntityMaps(); for (const objectMetadataEntity of objectMetadatas) { const flatObjectMetadata = fromObjectMetadataEntityToFlatObjectMetadata({ - ...objectMetadataEntity, - fields: fieldsByObjectId.get(objectMetadataEntity.id) || [], - indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [], - views: viewsByObjectId.get(objectMetadataEntity.id) || [], - } as ObjectMetadataEntity); + entity: { + ...objectMetadataEntity, + fields: fieldsByObjectId.get(objectMetadataEntity.id) || [], + indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [], + views: viewsByObjectId.get(objectMetadataEntity.id) || [], + }, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatObjectMetadata, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts index 4babb563e18..1413e01cf8e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type.ts @@ -1,6 +1,13 @@ import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -export type FlatObjectMetadata = FlatEntityFrom< +type BaseFlatObjectMetadata = FlatEntityFrom< Omit >; +export type FlatObjectMetadata = Omit & { + __universal?: BaseFlatObjectMetadata['__universal'] & { + // TODO remove once https://github.com/twentyhq/core-team-issues/issues/2172 has been resolved + labelIdentifierFieldMetadataUniversalIdentifier: string | null; + imageIdentifierFieldMetadataUniversalIdentifier: string | null; + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts index aea22d5f439..1430a2f203e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util.ts @@ -1,27 +1,99 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromObjectMetadataEntityToFlatObjectMetadata = ( - objectMetadataEntity: ObjectMetadataEntity, -): FlatObjectMetadata => { +type FromObjectMetadataEntityToFlatObjectMetadataArgs = + FromEntityToFlatEntityArgs<'objectMetadata'> & { + fieldMetadataIdToUniversalIdentifierMap: Map; + }; + +export const fromObjectMetadataEntityToFlatObjectMetadata = ({ + entity: objectMetadataEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, +}: FromObjectMetadataEntityToFlatObjectMetadataArgs): FlatObjectMetadata => { const objectMetadataEntityWithoutRelations = removePropertiesFromRecord( objectMetadataEntity, getMetadataEntityRelationProperties('objectMetadata'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + objectMetadataEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${objectMetadataEntity.applicationId} not found when building flat object metadata for object ${objectMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let labelIdentifierFieldMetadataUniversalIdentifier: string | null = null; + + if (isDefined(objectMetadataEntity.labelIdentifierFieldMetadataId)) { + labelIdentifierFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + objectMetadataEntity.labelIdentifierFieldMetadataId, + ) ?? null; + + if (!isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Label identifier field metadata with id ${objectMetadataEntity.labelIdentifierFieldMetadataId} not found when building flat object metadata for object ${objectMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let imageIdentifierFieldMetadataUniversalIdentifier: string | null = null; + + if (isDefined(objectMetadataEntity.imageIdentifierFieldMetadataId)) { + imageIdentifierFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + objectMetadataEntity.imageIdentifierFieldMetadataId, + ) ?? null; + + if (!isDefined(imageIdentifierFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Image identifier field metadata with id ${objectMetadataEntity.imageIdentifierFieldMetadataId} not found when building flat object metadata for object ${objectMetadataEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { ...objectMetadataEntityWithoutRelations, universalIdentifier: objectMetadataEntityWithoutRelations.universalIdentifier, createdAt: objectMetadataEntity.createdAt.toISOString(), updatedAt: objectMetadataEntity.updatedAt.toISOString(), - viewIds: objectMetadataEntity.views.map((viewEntity) => viewEntity.id), - indexMetadataIds: objectMetadataEntity.indexMetadatas.map( - (indexEntity) => indexEntity.id, - ), - fieldIds: objectMetadataEntity.fields.map((fieldEntity) => fieldEntity.id), + viewIds: objectMetadataEntity.views.map(({ id }) => id), + indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id), + fieldIds: objectMetadataEntity.fields.map(({ id }) => id), + __universal: { + universalIdentifier: + objectMetadataEntityWithoutRelations.universalIdentifier, + applicationUniversalIdentifier, + labelIdentifierFieldMetadataUniversalIdentifier, + imageIdentifierFieldMetadataUniversalIdentifier, + fieldUniversalIdentifiers: objectMetadataEntity.fields.map( + ({ universalIdentifier }) => universalIdentifier, + ), + indexMetadataUniversalIdentifiers: + objectMetadataEntity.indexMetadatas.map( + ({ universalIdentifier }) => universalIdentifier, + ), + viewUniversalIdentifiers: objectMetadataEntity.views.map( + ({ universalIdentifier }) => universalIdentifier, + ), + standardOverrides: objectMetadataEntityWithoutRelations.standardOverrides, + duplicateCriteria: objectMetadataEntityWithoutRelations.duplicateCriteria, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module.ts index 582e50ef9c6..0b13a5e9cad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/flat-page-layout-tab.module.ts @@ -1,14 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; +import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([PageLayoutTabEntity, PageLayoutWidgetEntity]), + TypeOrmModule.forFeature([ + PageLayoutTabEntity, + PageLayoutWidgetEntity, + ApplicationEntity, + PageLayoutEntity, + ]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatPageLayoutTabMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service.ts index d8dacdc50e9..8f2e36ea97d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service.ts @@ -5,12 +5,15 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type'; import { transformPageLayoutTabEntityToFlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; +import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -22,21 +25,37 @@ export class WorkspaceFlatPageLayoutTabMapCacheService extends WorkspaceCachePro private readonly pageLayoutTabRepository: Repository, @InjectRepository(PageLayoutWidgetEntity) private readonly pageLayoutWidgetRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(PageLayoutEntity) + private readonly pageLayoutRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const [pageLayoutTabs, pageLayoutWidgets] = await Promise.all([ - await this.pageLayoutTabRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - await this.pageLayoutWidgetRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - ]); + const [pageLayoutTabs, pageLayoutWidgets, applications, pageLayouts] = + await Promise.all([ + this.pageLayoutTabRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.pageLayoutWidgetRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'pageLayoutTabId'], + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.pageLayoutRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); const [pageLayoutWidgetsByPageLayoutTabId] = ( [ @@ -47,16 +66,25 @@ export class WorkspaceFlatPageLayoutTabMapCacheService extends WorkspaceCachePro ] as const ).map(regroupEntitiesByRelatedEntityId); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const pageLayoutIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(pageLayouts); + const flatPageLayoutTabMaps = createEmptyFlatEntityMaps(); for (const pageLayoutTabEntity of pageLayoutTabs) { const flatPageLayoutTab = transformPageLayoutTabEntityToFlatPageLayoutTab( { - ...pageLayoutTabEntity, - widgets: - pageLayoutWidgetsByPageLayoutTabId.get(pageLayoutTabEntity.id) || - [], - } as PageLayoutTabEntity, + entity: { + ...pageLayoutTabEntity, + widgets: + pageLayoutWidgetsByPageLayoutTabId.get(pageLayoutTabEntity.id) || + [], + }, + applicationIdToUniversalIdentifierMap, + pageLayoutIdToUniversalIdentifierMap, + }, ); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts index 095d6ae8bdd..9a0edabb096 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-tab/utils/transform-page-layout-tab-entity-to-flat-page-layout-tab.util.ts @@ -1,9 +1,39 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type'; -import { type PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const transformPageLayoutTabEntityToFlatPageLayoutTab = ({ + entity: pageLayoutTabEntity, + applicationIdToUniversalIdentifierMap, + pageLayoutIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'pageLayoutTab'>): FlatPageLayoutTab => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + pageLayoutTabEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${pageLayoutTabEntity.applicationId} not found for pageLayoutTab ${pageLayoutTabEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const pageLayoutUniversalIdentifier = + pageLayoutIdToUniversalIdentifierMap.get(pageLayoutTabEntity.pageLayoutId); + + if (!isDefined(pageLayoutUniversalIdentifier)) { + throw new FlatEntityMapsException( + `PageLayout with id ${pageLayoutTabEntity.pageLayoutId} not found for pageLayoutTab ${pageLayoutTabEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const transformPageLayoutTabEntityToFlatPageLayoutTab = ( - pageLayoutTabEntity: PageLayoutTabEntity, -): FlatPageLayoutTab => { return { createdAt: pageLayoutTabEntity.createdAt.toISOString(), deletedAt: pageLayoutTabEntity.deletedAt?.toISOString() ?? null, @@ -16,5 +46,13 @@ export const transformPageLayoutTabEntityToFlatPageLayoutTab = ( universalIdentifier: pageLayoutTabEntity.universalIdentifier, applicationId: pageLayoutTabEntity.applicationId, widgetIds: pageLayoutTabEntity.widgets.map((widget) => widget.id), + __universal: { + universalIdentifier: pageLayoutTabEntity.universalIdentifier, + applicationUniversalIdentifier, + pageLayoutUniversalIdentifier, + widgetUniversalIdentifiers: pageLayoutTabEntity.widgets.map( + (widget) => widget.universalIdentifier, + ), + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts index d8b380b3d37..22e40e377f1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module.ts @@ -1,12 +1,24 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FlatPageLayoutWidgetTypeValidatorService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/flat-page-layout-widget-type-validator.service'; import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; @Module({ - imports: [TypeOrmModule.forFeature([PageLayoutWidgetEntity])], + imports: [ + TypeOrmModule.forFeature([ + PageLayoutWidgetEntity, + ApplicationEntity, + PageLayoutTabEntity, + ObjectMetadataEntity, + FieldMetadataEntity, + ]), + ], providers: [ WorkspaceFlatPageLayoutWidgetMapCacheService, FlatPageLayoutWidgetTypeValidatorService, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts index d7323af313d..0f994379079 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service.ts @@ -5,11 +5,16 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type'; import { fromPageLayoutWidgetEntityToFlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,6 +23,14 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache constructor( @InjectRepository(PageLayoutWidgetEntity) private readonly pageLayoutWidgetRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(PageLayoutTabEntity) + private readonly pageLayoutTabRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, ) { super(); } @@ -25,21 +38,59 @@ export class WorkspaceFlatPageLayoutWidgetMapCacheService extends WorkspaceCache async computeForCache( workspaceId: string, ): Promise { - const existingPageLayoutWidgets = - await this.pageLayoutWidgetRepository.find({ - where: { - workspaceId, - }, + const [ + existingPageLayoutWidgets, + applications, + pageLayoutTabs, + objectMetadatas, + fieldMetadatas, + ] = await Promise.all([ + this.pageLayoutWidgetRepository.find({ + where: { workspaceId }, withDeleted: true, - }); + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.pageLayoutTabRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const pageLayoutTabIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(pageLayoutTabs); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); const flatPageLayoutWidgetMaps = createEmptyFlatEntityMaps(); for (const pageLayoutWidgetEntity of existingPageLayoutWidgets) { const flatPageLayoutWidget = - fromPageLayoutWidgetEntityToFlatPageLayoutWidget( - pageLayoutWidgetEntity, - ); + fromPageLayoutWidgetEntityToFlatPageLayoutWidget({ + entity: pageLayoutWidgetEntity, + applicationIdToUniversalIdentifierMap, + pageLayoutTabIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatPageLayoutWidget, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts new file mode 100644 index 00000000000..6bb9ee06e48 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util.ts @@ -0,0 +1,166 @@ +import { isDefined } from 'twenty-shared/utils'; + +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'; +import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; + +type PageLayoutWidgetConfiguration = PageLayoutWidgetEntity['configuration']; + +type UniversalPageLayoutWidgetConfiguration = NonNullable< + FlatPageLayoutWidget['__universal'] +>['configuration']; + +// Field metadata IDs in widget configurations don't need to reference existing entities +const getFieldMetadataUniversalIdentifier = ({ + fieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, +}: { + fieldMetadataId: string; + fieldMetadataIdToUniversalIdentifierMap: Map; +}): string | null => { + return fieldMetadataIdToUniversalIdentifierMap.get(fieldMetadataId) ?? null; +}; + +export const fromPageLayoutWidgetConfigurationToUniversalConfiguration = ({ + configuration, + fieldMetadataIdToUniversalIdentifierMap, +}: { + configuration: PageLayoutWidgetConfiguration; + fieldMetadataIdToUniversalIdentifierMap: Map; +}): UniversalPageLayoutWidgetConfiguration => { + switch (configuration.configurationType) { + case WidgetConfigurationType.AGGREGATE_CHART: { + const { aggregateFieldMetadataId, ratioAggregateConfig, ...rest } = + configuration; + + return { + ...rest, + aggregateFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: aggregateFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + ...(isDefined(ratioAggregateConfig) && { + ratioAggregateConfig: { + optionValue: ratioAggregateConfig.optionValue, + fieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: ratioAggregateConfig.fieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + }, + }), + }; + } + + case WidgetConfigurationType.GAUGE_CHART: { + const { aggregateFieldMetadataId, ...rest } = configuration; + + return { + ...rest, + aggregateFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: aggregateFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + }; + } + + case WidgetConfigurationType.PIE_CHART: { + const { aggregateFieldMetadataId, groupByFieldMetadataId, ...rest } = + configuration; + + return { + ...rest, + aggregateFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: aggregateFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + groupByFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: groupByFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + }; + } + + case WidgetConfigurationType.BAR_CHART: { + const { + aggregateFieldMetadataId, + primaryAxisGroupByFieldMetadataId, + secondaryAxisGroupByFieldMetadataId, + ...rest + } = configuration; + + return { + ...rest, + aggregateFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: aggregateFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + primaryAxisGroupByFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: primaryAxisGroupByFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + ...(isDefined(secondaryAxisGroupByFieldMetadataId) && { + secondaryAxisGroupByFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: secondaryAxisGroupByFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + }), + }; + } + + case WidgetConfigurationType.LINE_CHART: { + const { + aggregateFieldMetadataId, + primaryAxisGroupByFieldMetadataId, + secondaryAxisGroupByFieldMetadataId, + ...rest + } = configuration; + + return { + ...rest, + aggregateFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: aggregateFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + primaryAxisGroupByFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: primaryAxisGroupByFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + ...(isDefined(secondaryAxisGroupByFieldMetadataId) && { + secondaryAxisGroupByFieldMetadataUniversalIdentifier: + getFieldMetadataUniversalIdentifier({ + fieldMetadataId: secondaryAxisGroupByFieldMetadataId, + fieldMetadataIdToUniversalIdentifierMap, + }), + }), + }; + } + + case WidgetConfigurationType.VIEW: + case WidgetConfigurationType.FIELD: + case WidgetConfigurationType.FIELDS: + case WidgetConfigurationType.TIMELINE: + case WidgetConfigurationType.TASKS: + case WidgetConfigurationType.NOTES: + case WidgetConfigurationType.FILES: + case WidgetConfigurationType.EMAILS: + case WidgetConfigurationType.CALENDAR: + case WidgetConfigurationType.FIELD_RICH_TEXT: + case WidgetConfigurationType.WORKFLOW: + case WidgetConfigurationType.WORKFLOW_VERSION: + case WidgetConfigurationType.WORKFLOW_RUN: + case WidgetConfigurationType.FRONT_COMPONENT: + case WidgetConfigurationType.IFRAME: + case WidgetConfigurationType.STANDALONE_RICH_TEXT: + return configuration; + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts index dbee9ffc936..3ff09b0e182 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-entity-to-flat-page-layout-widget.util.ts @@ -1,17 +1,77 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ( - pageLayoutWidgetEntity: PageLayoutWidgetEntity, -): FlatPageLayoutWidget => { +type FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs = + FromEntityToFlatEntityArgs<'pageLayoutWidget'> & { + fieldMetadataIdToUniversalIdentifierMap: Map; + }; + +export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ({ + entity: pageLayoutWidgetEntity, + applicationIdToUniversalIdentifierMap, + pageLayoutTabIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, +}: FromPageLayoutWidgetEntityToFlatPageLayoutWidgetArgs): FlatPageLayoutWidget => { const pageLayoutWidgetEntityWithoutRelations = removePropertiesFromRecord( pageLayoutWidgetEntity, getMetadataEntityRelationProperties('pageLayoutWidget'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + pageLayoutWidgetEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${pageLayoutWidgetEntity.applicationId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const pageLayoutTabUniversalIdentifier = + pageLayoutTabIdToUniversalIdentifierMap.get( + pageLayoutWidgetEntity.pageLayoutTabId, + ); + + if (!isDefined(pageLayoutTabUniversalIdentifier)) { + throw new FlatEntityMapsException( + `PageLayoutTab with id ${pageLayoutWidgetEntity.pageLayoutTabId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let objectMetadataUniversalIdentifier: string | null = null; + + if (isDefined(pageLayoutWidgetEntity.objectMetadataId)) { + objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + pageLayoutWidgetEntity.objectMetadataId, + ) ?? null; + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${pageLayoutWidgetEntity.objectMetadataId} not found for pageLayoutWidget ${pageLayoutWidgetEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + const configurationWithUniversalIdentifiers = + fromPageLayoutWidgetConfigurationToUniversalConfiguration({ + configuration: pageLayoutWidgetEntityWithoutRelations.configuration, + fieldMetadataIdToUniversalIdentifierMap, + }); + return { ...pageLayoutWidgetEntityWithoutRelations, createdAt: pageLayoutWidgetEntity.createdAt.toISOString(), @@ -20,5 +80,13 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = ( universalIdentifier: pageLayoutWidgetEntityWithoutRelations.universalIdentifier, applicationId: pageLayoutWidgetEntityWithoutRelations.applicationId, + __universal: { + universalIdentifier: pageLayoutWidgetEntity.universalIdentifier, + applicationUniversalIdentifier, + pageLayoutTabUniversalIdentifier, + objectMetadataUniversalIdentifier, + gridPosition: pageLayoutWidgetEntityWithoutRelations.gridPosition, + configuration: configurationWithUniversalIdentifiers, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts index 5f7ca9e6725..de8d1ea7e37 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/flat-page-layout.module.ts @@ -1,12 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; @Module({ - imports: [TypeOrmModule.forFeature([PageLayoutEntity, PageLayoutTabEntity])], + imports: [ + TypeOrmModule.forFeature([ + PageLayoutEntity, + PageLayoutTabEntity, + ApplicationEntity, + ObjectMetadataEntity, + ]), + ], providers: [WorkspaceFlatPageLayoutMapCacheService], exports: [WorkspaceFlatPageLayoutMapCacheService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts index a436461d0d9..e1063f1d7b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service.ts @@ -5,12 +5,15 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type'; import { transformPageLayoutEntityToFlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -22,21 +25,37 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid private readonly pageLayoutRepository: Repository, @InjectRepository(PageLayoutTabEntity) private readonly pageLayoutTabRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const [pageLayouts, pageLayoutTabs] = await Promise.all([ - await this.pageLayoutRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - await this.pageLayoutTabRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - ]); + const [pageLayouts, pageLayoutTabs, applications, objectMetadatas] = + await Promise.all([ + this.pageLayoutRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.pageLayoutTabRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'pageLayoutId'], + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); const [pageLayoutTabsByPageLayoutId] = ( [ @@ -47,13 +66,22 @@ export class WorkspaceFlatPageLayoutMapCacheService extends WorkspaceCacheProvid ] as const ).map(regroupEntitiesByRelatedEntityId); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const flatPageLayoutMaps = createEmptyFlatEntityMaps(); for (const pageLayoutEntity of pageLayouts) { const flatPageLayout = transformPageLayoutEntityToFlatPageLayout({ - ...pageLayoutEntity, - tabs: pageLayoutTabsByPageLayoutId.get(pageLayoutEntity.id) || [], - } as PageLayoutEntity); + entity: { + ...pageLayoutEntity, + tabs: pageLayoutTabsByPageLayoutId.get(pageLayoutEntity.id) || [], + }, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatPageLayout, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts index 7c81a4e7ba1..d0c01e8580a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-page-layout/utils/transform-page-layout-entity-to-flat-page-layout.util.ts @@ -1,9 +1,43 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type'; -import { type PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const transformPageLayoutEntityToFlatPageLayout = ({ + entity: pageLayoutEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'pageLayout'>): FlatPageLayout => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(pageLayoutEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${pageLayoutEntity.applicationId} not found for pageLayout ${pageLayoutEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let objectMetadataUniversalIdentifier: string | null = null; + + if (isDefined(pageLayoutEntity.objectMetadataId)) { + objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + pageLayoutEntity.objectMetadataId, + ) ?? null; + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${pageLayoutEntity.objectMetadataId} not found for pageLayout ${pageLayoutEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } -export const transformPageLayoutEntityToFlatPageLayout = ( - pageLayoutEntity: PageLayoutEntity, -): FlatPageLayout => { return { createdAt: pageLayoutEntity.createdAt.toISOString(), deletedAt: pageLayoutEntity.deletedAt?.toISOString() ?? null, @@ -16,5 +50,13 @@ export const transformPageLayoutEntityToFlatPageLayout = ( universalIdentifier: pageLayoutEntity.universalIdentifier, applicationId: pageLayoutEntity.applicationId, tabIds: pageLayoutEntity.tabs.map((tab) => tab.id), + __universal: { + universalIdentifier: pageLayoutEntity.universalIdentifier, + applicationUniversalIdentifier, + objectMetadataUniversalIdentifier, + tabUniversalIdentifiers: pageLayoutEntity.tabs.map( + (tab) => tab.universalIdentifier, + ), + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts index 56cf71f936a..3bd1f9a1324 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts @@ -5,11 +5,14 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type'; -import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; +import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,23 +21,45 @@ export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvid constructor( @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const roleTargets = await this.roleTargetRepository.find({ - where: { - workspaceId, - }, - withDeleted: true, - }); + const [roleTargets, applications, roles] = await Promise.all([ + this.roleTargetRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.roleRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); const flatRoleTargetMaps = createEmptyFlatEntityMaps(); for (const roleTargetEntity of roleTargets) { - const flatRoleTarget = - fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity); + const flatRoleTarget = fromRoleTargetEntityToFlatRoleTarget({ + entity: roleTargetEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatRoleTarget, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts index 0def7de8d24..87e98b07fc2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util.ts @@ -1,19 +1,53 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type'; -import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromRoleTargetEntityToFlatRoleTarget = ({ + entity: roleTargetEntity, + applicationIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'roleTarget'>): FlatRoleTarget => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(roleTargetEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${roleTargetEntity.applicationId} not found when building flat role target for role target ${roleTargetEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get( + roleTargetEntity.roleId, + ); + + if (!isDefined(roleUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Role with id ${roleTargetEntity.roleId} not found when building flat role target for role target ${roleTargetEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromRoleTargetsEntityToFlatRoleTarget = ( - roleTarget: RoleTargetEntity, -): FlatRoleTarget => { return { - id: roleTarget.id, - workspaceId: roleTarget.workspaceId, - roleId: roleTarget.roleId, - userWorkspaceId: roleTarget.userWorkspaceId, - agentId: roleTarget.agentId, - apiKeyId: roleTarget.apiKeyId, - applicationId: roleTarget.applicationId, - universalIdentifier: roleTarget.universalIdentifier, - createdAt: roleTarget.createdAt.toISOString(), - updatedAt: roleTarget.updatedAt.toISOString(), + id: roleTargetEntity.id, + workspaceId: roleTargetEntity.workspaceId, + roleId: roleTargetEntity.roleId, + userWorkspaceId: roleTargetEntity.userWorkspaceId, + agentId: roleTargetEntity.agentId, + apiKeyId: roleTargetEntity.apiKeyId, + applicationId: roleTargetEntity.applicationId, + universalIdentifier: roleTargetEntity.universalIdentifier, + createdAt: roleTargetEntity.createdAt.toISOString(), + updatedAt: roleTargetEntity.updatedAt.toISOString(), + __universal: { + universalIdentifier: roleTargetEntity.universalIdentifier, + applicationUniversalIdentifier, + roleUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts index 9edaebbe507..bc5b1fac7fb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts @@ -1,36 +1,84 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type'; import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; -import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { type EntityManyToOneIdByUniversalIdentifierMaps } from 'src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type'; +import { type EntityWithRegroupedOneToManyRelations } from 'src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type'; +import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; + +type FromRoleEntityToFlatRoleArgs = { + entity: Omit< + EntityWithRegroupedOneToManyRelations>, + 'objectPermissions' | 'permissionFlags' | 'fieldPermissions' + > & { + objectPermissions: RegroupedEntity[]; + permissionFlags: RegroupedEntity[]; + fieldPermissions: RegroupedEntity[]; + }; +} & EntityManyToOneIdByUniversalIdentifierMaps<'role'>; + +export const fromRoleEntityToFlatRole = ({ + entity: roleEntity, + applicationIdToUniversalIdentifierMap, +}: FromRoleEntityToFlatRoleArgs): FlatRole => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(roleEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${roleEntity.applicationId} not found for role ${roleEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => { return { - id: role.id, - standardId: role.standardId, - label: role.label, - description: role.description, - icon: role.icon, - isEditable: role.isEditable, - canUpdateAllSettings: role.canUpdateAllSettings, - canAccessAllTools: role.canAccessAllTools, - canReadAllObjectRecords: role.canReadAllObjectRecords, - canUpdateAllObjectRecords: role.canUpdateAllObjectRecords, - canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords, - canDestroyAllObjectRecords: role.canDestroyAllObjectRecords, - canBeAssignedToUsers: role.canBeAssignedToUsers, - canBeAssignedToAgents: role.canBeAssignedToAgents, - canBeAssignedToApiKeys: role.canBeAssignedToApiKeys, - workspaceId: role.workspaceId, - createdAt: role.createdAt.toISOString(), - updatedAt: role.updatedAt.toISOString(), - universalIdentifier: role.universalIdentifier, - applicationId: role.applicationId, - roleTargetIds: role.roleTargets.map((rt) => rt.id), - objectPermissionIds: role.objectPermissions.map((op) => op.id), - permissionFlagIds: role.permissionFlags.map((pf) => pf.id), - fieldPermissionIds: role.fieldPermissions.map((fp) => fp.id), - rowLevelPermissionPredicateIds: role.rowLevelPermissionPredicates.map( - (rp) => rp.id, + id: roleEntity.id, + standardId: roleEntity.standardId, + label: roleEntity.label, + description: roleEntity.description, + icon: roleEntity.icon, + isEditable: roleEntity.isEditable, + canUpdateAllSettings: roleEntity.canUpdateAllSettings, + canAccessAllTools: roleEntity.canAccessAllTools, + canReadAllObjectRecords: roleEntity.canReadAllObjectRecords, + canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords: roleEntity.canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords, + canBeAssignedToUsers: roleEntity.canBeAssignedToUsers, + canBeAssignedToAgents: roleEntity.canBeAssignedToAgents, + canBeAssignedToApiKeys: roleEntity.canBeAssignedToApiKeys, + workspaceId: roleEntity.workspaceId, + createdAt: roleEntity.createdAt.toISOString(), + updatedAt: roleEntity.updatedAt.toISOString(), + universalIdentifier: roleEntity.universalIdentifier, + applicationId: roleEntity.applicationId, + roleTargetIds: roleEntity.roleTargets.map(({ id }) => id), + objectPermissionIds: roleEntity.objectPermissions.map(({ id }) => id), + permissionFlagIds: roleEntity.permissionFlags.map(({ id }) => id), + fieldPermissionIds: roleEntity.fieldPermissions.map(({ id }) => id), + rowLevelPermissionPredicateIds: roleEntity.rowLevelPermissionPredicates.map( + ({ id }) => id, ), rowLevelPermissionPredicateGroupIds: - role.rowLevelPermissionPredicateGroups.map((rp) => rp.id), + roleEntity.rowLevelPermissionPredicateGroups.map(({ id }) => id), + __universal: { + universalIdentifier: roleEntity.universalIdentifier, + applicationUniversalIdentifier, + roleTargetUniversalIdentifiers: roleEntity.roleTargets.map( + ({ universalIdentifier }) => universalIdentifier, + ), + rowLevelPermissionPredicateUniversalIdentifiers: + roleEntity.rowLevelPermissionPredicates.map( + ({ universalIdentifier }) => universalIdentifier, + ), + rowLevelPermissionPredicateGroupUniversalIdentifiers: + roleEntity.rowLevelPermissionPredicateGroups.map( + ({ universalIdentifier }) => universalIdentifier, + ), + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service.ts index 09abb555191..c494e2b1b9d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service.ts @@ -7,11 +7,17 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; +import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; +import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -20,6 +26,14 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend constructor( @InjectRepository(RowLevelPermissionPredicateGroupEntity) private readonly rowLevelPermissionPredicateGroupRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + @InjectRepository(RowLevelPermissionPredicateEntity) + private readonly rowLevelPermissionPredicateRepository: Repository, ) { super(); } @@ -27,13 +41,67 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend async computeForCache( workspaceId: string, ): Promise { - const rowLevelPermissionPredicateGroups = - await this.rowLevelPermissionPredicateGroupRepository.find({ - where: { - workspaceId, - }, + const [ + rowLevelPermissionPredicateGroups, + applications, + objectMetadatas, + roles, + rowLevelPermissionPredicates, + ] = await Promise.all([ + this.rowLevelPermissionPredicateGroupRepository.find({ + where: { workspaceId }, withDeleted: true, - }); + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.roleRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.rowLevelPermissionPredicateRepository.find({ + where: { workspaceId }, + select: [ + 'id', + 'universalIdentifier', + 'rowLevelPermissionPredicateGroupId', + ], + withDeleted: true, + }), + ]); + + const [ + childRowLevelPermissionPredicateGroupsByParentId, + rowLevelPermissionPredicatesByGroupId, + ] = ( + [ + { + entities: rowLevelPermissionPredicateGroups, + foreignKey: 'parentRowLevelPermissionPredicateGroupId', + }, + { + entities: rowLevelPermissionPredicates, + foreignKey: 'rowLevelPermissionPredicateGroupId', + }, + ] as const + ).map(regroupEntitiesByRelatedEntityId); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); + const rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(rowLevelPermissionPredicateGroups); const flatRowLevelPermissionPredicateGroupMaps = createEmptyFlatEntityMaps(); @@ -41,7 +109,23 @@ export class WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService extend for (const rowLevelPermissionPredicateGroupEntity of rowLevelPermissionPredicateGroups) { const flatRowLevelPermissionPredicateGroup = fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup( - rowLevelPermissionPredicateGroupEntity, + { + entity: { + ...rowLevelPermissionPredicateGroupEntity, + childRowLevelPermissionPredicateGroups: + childRowLevelPermissionPredicateGroupsByParentId.get( + rowLevelPermissionPredicateGroupEntity.id, + ) || [], + rowLevelPermissionPredicates: + rowLevelPermissionPredicatesByGroupId.get( + rowLevelPermissionPredicateGroupEntity.id, + ) || [], + }, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap, + }, ); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service.ts index 8ae68932858..436e4decb2c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service.ts @@ -7,11 +7,17 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -20,6 +26,16 @@ export class WorkspaceFlatRowLevelPermissionPredicateMapCacheService extends Wor constructor( @InjectRepository(RowLevelPermissionPredicateEntity) private readonly rowLevelPermissionPredicateRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + @InjectRepository(RowLevelPermissionPredicateGroupEntity) + private readonly rowLevelPermissionPredicateGroupRepository: Repository, ) { super(); } @@ -27,21 +43,68 @@ export class WorkspaceFlatRowLevelPermissionPredicateMapCacheService extends Wor async computeForCache( workspaceId: string, ): Promise { - const rowLevelPermissionPredicates = - await this.rowLevelPermissionPredicateRepository.find({ - where: { - workspaceId, - }, + const [ + rowLevelPermissionPredicates, + applications, + fieldMetadatas, + objectMetadatas, + roles, + rowLevelPermissionPredicateGroups, + ] = await Promise.all([ + this.rowLevelPermissionPredicateRepository.find({ + where: { workspaceId }, withDeleted: true, - }); + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.roleRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.rowLevelPermissionPredicateGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); + const objectMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(objectMetadatas); + const roleIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(roles); + const rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(rowLevelPermissionPredicateGroups); const flatRowLevelPermissionPredicateMaps = createEmptyFlatEntityMaps(); for (const rowLevelPermissionPredicateEntity of rowLevelPermissionPredicates) { const flatRowLevelPermissionPredicate = - fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate( - rowLevelPermissionPredicateEntity, - ); + fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate({ + entity: rowLevelPermissionPredicateEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatRowLevelPermissionPredicate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util.ts index 5c2059c8e58..be17c2083d2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-entity-to-flat-row-level-permission-predicate.util.ts @@ -1,20 +1,117 @@ /* @license Enterprise */ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/row-level-permission-predicate-entity-relation-properties.constant'; -import { type RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; export const fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredicate = - ( - rowLevelPermissionPredicateEntity: RowLevelPermissionPredicateEntity, - ): FlatRowLevelPermissionPredicate => { + ({ + entity: rowLevelPermissionPredicateEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap, + }: FromEntityToFlatEntityArgs<'rowLevelPermissionPredicate'>): FlatRowLevelPermissionPredicate => { const rowLevelPermissionPredicateEntityWithoutRelations = removePropertiesFromRecord(rowLevelPermissionPredicateEntity, [ ...ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES, ] as (typeof ROW_LEVEL_PERMISSION_PREDICATE_ENTITY_RELATION_PROPERTIES)[number][]); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${rowLevelPermissionPredicateEntity.applicationId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const fieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${rowLevelPermissionPredicateEntity.fieldMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.objectMetadataId, + ); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${rowLevelPermissionPredicateEntity.objectMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.roleId, + ); + + if (!isDefined(roleUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Role with id ${rowLevelPermissionPredicateEntity.roleId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let workspaceMemberFieldMetadataUniversalIdentifier: string | null = null; + + if ( + isDefined( + rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId, + ) + ) { + workspaceMemberFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId, + ) ?? null; + + if (!isDefined(workspaceMemberFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${rowLevelPermissionPredicateEntity.workspaceMemberFieldMetadataId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let rowLevelPermissionPredicateGroupUniversalIdentifier: string | null = + null; + + if ( + isDefined( + rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId, + ) + ) { + rowLevelPermissionPredicateGroupUniversalIdentifier = + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId, + ) ?? null; + + if (!isDefined(rowLevelPermissionPredicateGroupUniversalIdentifier)) { + throw new FlatEntityMapsException( + `RowLevelPermissionPredicateGroup with id ${rowLevelPermissionPredicateEntity.rowLevelPermissionPredicateGroupId} not found for rowLevelPermissionPredicate ${rowLevelPermissionPredicateEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { ...rowLevelPermissionPredicateEntityWithoutRelations, createdAt: rowLevelPermissionPredicateEntity.createdAt.toISOString(), @@ -23,5 +120,16 @@ export const fromRowLevelPermissionPredicateEntityToFlatRowLevelPermissionPredic rowLevelPermissionPredicateEntity.deletedAt?.toISOString() ?? null, universalIdentifier: rowLevelPermissionPredicateEntityWithoutRelations.universalIdentifier, + __universal: { + universalIdentifier: + rowLevelPermissionPredicateEntity.universalIdentifier, + applicationUniversalIdentifier, + fieldMetadataUniversalIdentifier, + objectMetadataUniversalIdentifier, + roleUniversalIdentifier, + workspaceMemberFieldMetadataUniversalIdentifier, + rowLevelPermissionPredicateGroupUniversalIdentifier, + value: rowLevelPermissionPredicateEntityWithoutRelations.value, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util.ts index e0febae41a0..741554236f5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-row-level-permission-predicate/utils/from-row-level-permission-predicate-group-entity-to-flat-row-level-permission-predicate-group.util.ts @@ -1,20 +1,95 @@ /* @license Enterprise */ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/row-level-permission-predicate-group-entity-relation-properties.constant'; -import { type RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +type FromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroupArgs = + FromEntityToFlatEntityArgs<'rowLevelPermissionPredicateGroup'> & { + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap: Map< + string, + string + >; + }; export const fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroup = - ( - rowLevelPermissionPredicateGroupEntity: RowLevelPermissionPredicateGroupEntity, - ): FlatRowLevelPermissionPredicateGroup => { + ({ + entity: rowLevelPermissionPredicateGroupEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + roleIdToUniversalIdentifierMap, + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap, + }: FromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionPredicateGroupArgs): FlatRowLevelPermissionPredicateGroup => { const rowLevelPermissionPredicateGroupEntityWithoutRelations = removePropertiesFromRecord(rowLevelPermissionPredicateGroupEntity, [ ...ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES, ] as (typeof ROW_LEVEL_PERMISSION_PREDICATE_GROUP_ENTITY_RELATION_PROPERTIES)[number][]); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateGroupEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${rowLevelPermissionPredicateGroupEntity.applicationId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateGroupEntity.objectMetadataId, + ); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${rowLevelPermissionPredicateGroupEntity.objectMetadataId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateGroupEntity.roleId, + ); + + if (!isDefined(roleUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Role with id ${rowLevelPermissionPredicateGroupEntity.roleId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let parentRowLevelPermissionPredicateGroupUniversalIdentifier: + | string + | null = null; + + if ( + isDefined( + rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId, + ) + ) { + parentRowLevelPermissionPredicateGroupUniversalIdentifier = + rowLevelPermissionPredicateGroupIdToUniversalIdentifierMap.get( + rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId, + ) ?? null; + + if ( + !isDefined(parentRowLevelPermissionPredicateGroupUniversalIdentifier) + ) { + throw new FlatEntityMapsException( + `RowLevelPermissionPredicateGroup with id ${rowLevelPermissionPredicateGroupEntity.parentRowLevelPermissionPredicateGroupId} not found for rowLevelPermissionPredicateGroup ${rowLevelPermissionPredicateGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { ...rowLevelPermissionPredicateGroupEntityWithoutRelations, createdAt: rowLevelPermissionPredicateGroupEntity.createdAt.toISOString(), @@ -31,5 +106,21 @@ export const fromRowLevelPermissionPredicateGroupEntityToFlatRowLevelPermissionP rowLevelPermissionPredicateGroupEntity.rowLevelPermissionPredicates ?? [] ).map(({ id }) => id), + __universal: { + universalIdentifier: + rowLevelPermissionPredicateGroupEntity.universalIdentifier, + applicationUniversalIdentifier, + objectMetadataUniversalIdentifier, + roleUniversalIdentifier, + parentRowLevelPermissionPredicateGroupUniversalIdentifier, + childRowLevelPermissionPredicateGroupUniversalIdentifiers: ( + rowLevelPermissionPredicateGroupEntity.childRowLevelPermissionPredicateGroups ?? + [] + ).map(({ universalIdentifier }) => universalIdentifier), + rowLevelPermissionPredicateUniversalIdentifiers: ( + rowLevelPermissionPredicateGroupEntity.rowLevelPermissionPredicates ?? + [] + ).map(({ universalIdentifier }) => universalIdentifier), + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-skill/flat-skill.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-skill/flat-skill.module.ts index 1c57c2d326a..dc0817bb82c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-skill/flat-skill.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-skill/flat-skill.module.ts @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceFlatSkillMapCacheService } from 'src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service'; import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([SkillEntity]), + TypeOrmModule.forFeature([ApplicationEntity, SkillEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatSkillMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service.ts index ffb4fdcdffe..601c9ee4f11 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-skill/services/workspace-flat-skill-map-cache.service.ts @@ -5,11 +5,13 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; 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 { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,20 +20,35 @@ export class WorkspaceFlatSkillMapCacheService extends WorkspaceCacheProvider, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const skills = await this.skillRepository.find({ - where: { workspaceId }, - withDeleted: true, - }); + const [skills, applications] = await Promise.all([ + this.skillRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); const flatSkillMaps = createEmptyFlatEntityMaps(); for (const skillEntity of skills) { - const flatSkill = fromSkillEntityToFlatSkill(skillEntity); + const flatSkill = fromSkillEntityToFlatSkill({ + entity: skillEntity, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatSkill, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util.ts index 1a1014dc266..97c056a9d8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util.ts @@ -1,9 +1,26 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromSkillEntityToFlatSkill = ({ + entity: skillEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'skill'>): FlatSkill => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(skillEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${skillEntity.applicationId} not found for skill ${skillEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromSkillEntityToFlatSkill = ( - skillEntity: SkillEntity, -): FlatSkill => { return { createdAt: skillEntity.createdAt.toISOString(), updatedAt: skillEntity.updatedAt.toISOString(), @@ -19,5 +36,9 @@ export const fromSkillEntityToFlatSkill = ( isActive: skillEntity.isActive, universalIdentifier: skillEntity.universalIdentifier, applicationId: skillEntity.applicationId, + __universal: { + universalIdentifier: skillEntity.universalIdentifier, + applicationUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts index d80e2c2aac4..9358081f706 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts @@ -5,11 +5,15 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; import { fromViewFieldEntityToFlatViewField } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,22 +22,57 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide constructor( @InjectRepository(ViewFieldEntity) private readonly viewFieldRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const existingViewFields = await this.viewFieldRepository.find({ - where: { - workspaceId, - }, - withDeleted: true, - }); + const [viewFields, applications, fieldMetadatas, views] = await Promise.all( + [ + this.viewFieldRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ], + ); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); const flatViewFieldMaps = createEmptyFlatEntityMaps(); - for (const viewFieldEntity of existingViewFields) { - const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity); + for (const viewFieldEntity of viewFields) { + const flatViewField = fromViewFieldEntityToFlatViewField({ + entity: viewFieldEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatViewField, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts index 0d230da1ab2..36c3e493456 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util.ts @@ -1,22 +1,68 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromViewFieldEntityToFlatViewField = ( - viewFieldEntity: ViewFieldEntity, -): FlatViewField => { +export const fromViewFieldEntityToFlatViewField = ({ + entity: viewFieldEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'viewField'>): FlatViewField => { const viewFieldEntityWithoutRelations = removePropertiesFromRecord( viewFieldEntity, getMetadataEntityRelationProperties('viewField'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(viewFieldEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewFieldEntity.applicationId} not found for viewField ${viewFieldEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const fieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + viewFieldEntity.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${viewFieldEntity.fieldMetadataId} not found for viewField ${viewFieldEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewFieldEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewFieldEntity.viewId} not found for viewField ${viewFieldEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + return { ...viewFieldEntityWithoutRelations, createdAt: viewFieldEntity.createdAt.toISOString(), updatedAt: viewFieldEntity.updatedAt.toISOString(), deletedAt: viewFieldEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewFieldEntityWithoutRelations.universalIdentifier, + __universal: { + universalIdentifier: viewFieldEntity.universalIdentifier, + applicationUniversalIdentifier, + fieldMetadataUniversalIdentifier, + viewUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service.ts index 2bb00fa5a3b..6ea6266bfa9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service.ts @@ -5,12 +5,15 @@ import { type Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatViewFilterGroupMaps } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group-maps.type'; import { fromViewFilterGroupEntityToFlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -20,24 +23,39 @@ export class WorkspaceFlatViewFilterGroupMapCacheService extends WorkspaceCacheP constructor( @InjectRepository(ViewFilterGroupEntity) private readonly viewFilterGroupRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, @InjectRepository(ViewFilterEntity) private readonly viewFilterRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const [viewFilterGroups, viewFilters] = await Promise.all([ - this.viewFilterGroupRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - this.viewFilterRepository.find({ - where: { workspaceId }, - select: ['id', 'viewFilterGroupId'], - withDeleted: true, - }), - ]); + const [viewFilterGroups, applications, viewFilters, views] = + await Promise.all([ + this.viewFilterGroupRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewFilterRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewFilterGroupId'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); const [viewFiltersByViewFilterGroupId, childViewFilterGroupsByParentId] = ( [ @@ -52,17 +70,31 @@ export class WorkspaceFlatViewFilterGroupMapCacheService extends WorkspaceCacheP ] as const ).map(regroupEntitiesByRelatedEntityId); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const viewFilterGroupIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(viewFilterGroups); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); + const flatViewFilterGroupMaps = createEmptyFlatEntityMaps(); for (const viewFilterGroupEntity of viewFilterGroups) { const flatViewFilterGroup = fromViewFilterGroupEntityToFlatViewFilterGroup({ - ...viewFilterGroupEntity, - viewFilters: - viewFiltersByViewFilterGroupId.get(viewFilterGroupEntity.id) || [], - childViewFilterGroups: - childViewFilterGroupsByParentId.get(viewFilterGroupEntity.id) || [], - } as ViewFilterGroupEntity); + entity: { + ...viewFilterGroupEntity, + viewFilters: + viewFiltersByViewFilterGroupId.get(viewFilterGroupEntity.id) || + [], + childViewFilterGroups: + childViewFilterGroupsByParentId.get(viewFilterGroupEntity.id) || + [], + }, + applicationIdToUniversalIdentifierMap, + viewFilterGroupIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatViewFilterGroup, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util.ts index f82bac627df..7b112210270 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter-group/utils/from-view-filter-group-entity-to-flat-view-filter-group.util.ts @@ -1,17 +1,68 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromViewFilterGroupEntityToFlatViewFilterGroup = ( - viewFilterGroupEntity: ViewFilterGroupEntity, -): FlatViewFilterGroup => { +type FromViewFilterGroupEntityToFlatViewFilterGroupArgs = + FromEntityToFlatEntityArgs<'viewFilterGroup'> & { + viewFilterGroupIdToUniversalIdentifierMap: Map; + }; + +export const fromViewFilterGroupEntityToFlatViewFilterGroup = ({ + entity: viewFilterGroupEntity, + applicationIdToUniversalIdentifierMap, + viewFilterGroupIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromViewFilterGroupEntityToFlatViewFilterGroupArgs): FlatViewFilterGroup => { const viewFilterGroupEntityWithoutRelations = removePropertiesFromRecord( viewFilterGroupEntity, getMetadataEntityRelationProperties('viewFilterGroup'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + viewFilterGroupEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewFilterGroupEntity.applicationId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let parentViewFilterGroupUniversalIdentifier: string | null = null; + + if (isDefined(viewFilterGroupEntity.parentViewFilterGroupId)) { + parentViewFilterGroupUniversalIdentifier = + viewFilterGroupIdToUniversalIdentifierMap.get( + viewFilterGroupEntity.parentViewFilterGroupId, + ) ?? null; + + if (!isDefined(parentViewFilterGroupUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ViewFilterGroup with id ${viewFilterGroupEntity.parentViewFilterGroupId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewFilterGroupEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewFilterGroupEntity.viewId} not found for viewFilterGroup ${viewFilterGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + return { ...viewFilterGroupEntityWithoutRelations, createdAt: viewFilterGroupEntity.createdAt.toISOString(), @@ -19,12 +70,22 @@ export const fromViewFilterGroupEntityToFlatViewFilterGroup = ( deletedAt: viewFilterGroupEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewFilterGroupEntityWithoutRelations.universalIdentifier, - viewFilterIds: - viewFilterGroupEntity.viewFilters?.map((viewFilter) => viewFilter.id) ?? - [], + viewFilterIds: viewFilterGroupEntity.viewFilters?.map(({ id }) => id) ?? [], childViewFilterGroupIds: - viewFilterGroupEntity.childViewFilterGroups?.map( - (childGroup) => childGroup.id, - ) ?? [], + viewFilterGroupEntity.childViewFilterGroups?.map(({ id }) => id) ?? [], + __universal: { + universalIdentifier: viewFilterGroupEntity.universalIdentifier, + applicationUniversalIdentifier, + parentViewFilterGroupUniversalIdentifier, + viewUniversalIdentifier, + viewFilterUniversalIdentifiers: + viewFilterGroupEntity.viewFilters?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], + childViewFilterGroupUniversalIdentifiers: + viewFilterGroupEntity.childViewFilterGroups?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts index 7aacbb4ba38..3366e87468b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts @@ -5,11 +5,16 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type'; import { fromViewFilterEntityToFlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util'; +import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,23 +23,66 @@ export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvid constructor( @InjectRepository(ViewFilterEntity) private readonly viewFilterRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ViewFilterGroupEntity) + private readonly viewFilterGroupRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const viewFilters = await this.viewFilterRepository.find({ - where: { - workspaceId, - }, - withDeleted: true, - }); + const [viewFilters, applications, fieldMetadatas, viewFilterGroups, views] = + await Promise.all([ + this.viewFilterRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewFilterGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const fieldMetadataIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(fieldMetadatas); + const viewFilterGroupIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(viewFilterGroups); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); const flatViewFilterMaps = createEmptyFlatEntityMaps(); for (const viewFilterEntity of viewFilters) { - const flatViewFilter = - fromViewFilterEntityToFlatViewFilter(viewFilterEntity); + const flatViewFilter = fromViewFilterEntityToFlatViewFilter({ + entity: viewFilterEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + viewFilterGroupIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatViewFilter, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util.ts index b83cd4784be..88421d3d6a1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util.ts @@ -1,22 +1,87 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromViewFilterEntityToFlatViewFilter = ( - viewFilterEntity: ViewFilterEntity, -): FlatViewFilter => { +export const fromViewFilterEntityToFlatViewFilter = ({ + entity: viewFilterEntity, + applicationIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, + viewFilterGroupIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'viewFilter'>): FlatViewFilter => { const viewFilterEntityWithoutRelations = removePropertiesFromRecord( viewFilterEntity, getMetadataEntityRelationProperties('viewFilter'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(viewFilterEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewFilterEntity.applicationId} not found for viewFilter ${viewFilterEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const fieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + viewFilterEntity.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${viewFilterEntity.fieldMetadataId} not found for viewFilter ${viewFilterEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let viewFilterGroupUniversalIdentifier: string | null = null; + + if (isDefined(viewFilterEntity.viewFilterGroupId)) { + viewFilterGroupUniversalIdentifier = + viewFilterGroupIdToUniversalIdentifierMap.get( + viewFilterEntity.viewFilterGroupId, + ) ?? null; + + if (!isDefined(viewFilterGroupUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ViewFilterGroup with id ${viewFilterEntity.viewFilterGroupId} not found for viewFilter ${viewFilterEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewFilterEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewFilterEntity.viewId} not found for viewFilter ${viewFilterEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + return { ...viewFilterEntityWithoutRelations, createdAt: viewFilterEntity.createdAt.toISOString(), updatedAt: viewFilterEntity.updatedAt.toISOString(), deletedAt: viewFilterEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewFilterEntityWithoutRelations.universalIdentifier, + __universal: { + universalIdentifier: viewFilterEntity.universalIdentifier, + applicationUniversalIdentifier, + fieldMetadataUniversalIdentifier, + viewFilterGroupUniversalIdentifier, + viewUniversalIdentifier, + value: viewFilterEntityWithoutRelations.value, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts index 96190ce4910..e122d83c39c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts @@ -5,11 +5,14 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type'; import { fromViewGroupEntityToFlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; +import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,22 +21,45 @@ export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceCacheProvide constructor( @InjectRepository(ViewGroupEntity) private readonly viewGroupRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const existingViewGroups = await this.viewGroupRepository.find({ - where: { - workspaceId, - }, - withDeleted: true, - }); + const [viewGroups, applications, views] = await Promise.all([ + this.viewGroupRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const viewIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(views); const flatViewGroupMaps = createEmptyFlatEntityMaps(); - for (const viewGroupEntity of existingViewGroups) { - const flatViewGroup = fromViewGroupEntityToFlatViewGroup(viewGroupEntity); + for (const viewGroupEntity of viewGroups) { + const flatViewGroup = fromViewGroupEntityToFlatViewGroup({ + entity: viewGroupEntity, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatViewGroup, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util.ts index f6bd3fb1dfe..1041b455950 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util.ts @@ -1,22 +1,54 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; 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'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; -export const fromViewGroupEntityToFlatViewGroup = ( - viewGroupEntity: ViewGroupEntity, -): FlatViewGroup => { +export const fromViewGroupEntityToFlatViewGroup = ({ + entity: viewGroupEntity, + applicationIdToUniversalIdentifierMap, + viewIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'viewGroup'>): FlatViewGroup => { const viewGroupEntityWithoutRelations = removePropertiesFromRecord( viewGroupEntity, getMetadataEntityRelationProperties('viewGroup'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(viewGroupEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewGroupEntity.applicationId} not found for viewGroup ${viewGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const viewUniversalIdentifier = viewIdToUniversalIdentifierMap.get( + viewGroupEntity.viewId, + ); + + if (!isDefined(viewUniversalIdentifier)) { + throw new FlatEntityMapsException( + `View with id ${viewGroupEntity.viewId} not found for viewGroup ${viewGroupEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + return { ...viewGroupEntityWithoutRelations, createdAt: viewGroupEntity.createdAt.toISOString(), updatedAt: viewGroupEntity.updatedAt.toISOString(), deletedAt: viewGroupEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewGroupEntityWithoutRelations.universalIdentifier, + __universal: { + universalIdentifier: viewGroupEntity.universalIdentifier, + applicationUniversalIdentifier, + viewUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts index fc64d076898..533c6b6ad8d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/flat-view.module.ts @@ -1,7 +1,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -11,6 +14,9 @@ import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entit @Module({ imports: [ TypeOrmModule.forFeature([ + ApplicationEntity, + FieldMetadataEntity, + ObjectMetadataEntity, ViewEntity, ViewFieldEntity, ViewFilterEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts index 3081da09bce..5cf0982e64b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts @@ -5,15 +5,19 @@ import { type Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; import { fromViewEntityToFlatView } from 'src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -23,6 +27,12 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, @InjectRepository(ViewFieldEntity) private readonly viewFieldRepository: Repository, @InjectRepository(ViewFilterEntity) @@ -36,33 +46,56 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider { - const [views, viewFields, viewFilters, viewGroups, viewFilterGroups] = - await Promise.all([ - this.viewRepository.find({ - where: { workspaceId }, - withDeleted: true, - }), - this.viewFieldRepository.find({ - where: { workspaceId }, - select: ['id', 'viewId'], - withDeleted: true, - }), - this.viewFilterRepository.find({ - where: { workspaceId }, - select: ['id', 'viewId'], - withDeleted: true, - }), - this.viewGroupRepository.find({ - where: { workspaceId }, - select: ['id', 'viewId'], - withDeleted: true, - }), - this.viewFilterGroupRepository.find({ - where: { workspaceId }, - select: ['id', 'viewId'], - withDeleted: true, - }), - ]); + const [ + views, + applications, + objectMetadatas, + fieldMetadatas, + viewFields, + viewFilters, + viewGroups, + viewFilterGroups, + ] = await Promise.all([ + this.viewRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.objectMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.fieldMetadataRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), + this.viewFieldRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewId'], + withDeleted: true, + }), + this.viewFilterRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewId'], + withDeleted: true, + }), + this.viewGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewId'], + withDeleted: true, + }), + this.viewFilterGroupRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier', 'viewId'], + withDeleted: true, + }), + ]); const [ viewFieldsByViewId, @@ -90,16 +123,28 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider { +export const fromViewEntityToFlatView = ({ + entity: viewEntity, + applicationIdToUniversalIdentifierMap, + objectMetadataIdToUniversalIdentifierMap, + fieldMetadataIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'view'>): FlatView => { const viewEntityWithoutRelations = removePropertiesFromRecord( viewEntity, getMetadataEntityRelationProperties('view'), ); + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(viewEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${viewEntity.applicationId} not found for view ${viewEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const objectMetadataUniversalIdentifier = + objectMetadataIdToUniversalIdentifierMap.get(viewEntity.objectMetadataId); + + if (!isDefined(objectMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `ObjectMetadata with id ${viewEntity.objectMetadataId} not found for view ${viewEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + let kanbanAggregateOperationFieldMetadataUniversalIdentifier: string | null = + null; + + if (isDefined(viewEntity.kanbanAggregateOperationFieldMetadataId)) { + kanbanAggregateOperationFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + viewEntity.kanbanAggregateOperationFieldMetadataId, + ) ?? null; + + if (!isDefined(kanbanAggregateOperationFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${viewEntity.kanbanAggregateOperationFieldMetadataId} not found for view ${viewEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let calendarFieldMetadataUniversalIdentifier: string | null = null; + + if (isDefined(viewEntity.calendarFieldMetadataId)) { + calendarFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + viewEntity.calendarFieldMetadataId, + ) ?? null; + + if (!isDefined(calendarFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${viewEntity.calendarFieldMetadataId} not found for view ${viewEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + + let mainGroupByFieldMetadataUniversalIdentifier: string | null = null; + + if (isDefined(viewEntity.mainGroupByFieldMetadataId)) { + mainGroupByFieldMetadataUniversalIdentifier = + fieldMetadataIdToUniversalIdentifierMap.get( + viewEntity.mainGroupByFieldMetadataId, + ) ?? null; + + if (!isDefined(mainGroupByFieldMetadataUniversalIdentifier)) { + throw new FlatEntityMapsException( + `FieldMetadata with id ${viewEntity.mainGroupByFieldMetadataId} not found for view ${viewEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + } + return { ...viewEntityWithoutRelations, createdAt: viewEntity.createdAt.toISOString(), updatedAt: viewEntity.updatedAt.toISOString(), deletedAt: viewEntity.deletedAt?.toISOString() ?? null, universalIdentifier: viewEntityWithoutRelations.universalIdentifier, - viewFieldIds: viewEntity.viewFields.map((viewField) => viewField.id), - viewFilterIds: viewEntity.viewFilters.map((viewFilter) => viewFilter.id), - viewGroupIds: viewEntity.viewGroups.map((viewGroup) => viewGroup.id), - viewFilterGroupIds: - viewEntity.viewFilterGroups?.map( - (viewFilterGroup) => viewFilterGroup.id, - ) ?? [], + viewFieldIds: viewEntity.viewFields.map(({ id }) => id), + viewFilterIds: viewEntity.viewFilters.map(({ id }) => id), + viewGroupIds: viewEntity.viewGroups.map(({ id }) => id), + viewFilterGroupIds: viewEntity.viewFilterGroups?.map(({ id }) => id) ?? [], + __universal: { + universalIdentifier: viewEntity.universalIdentifier, + applicationUniversalIdentifier, + objectMetadataUniversalIdentifier, + kanbanAggregateOperationFieldMetadataUniversalIdentifier, + calendarFieldMetadataUniversalIdentifier, + mainGroupByFieldMetadataUniversalIdentifier, + viewFieldUniversalIdentifiers: viewEntity.viewFields.map( + ({ universalIdentifier }) => universalIdentifier, + ), + viewFilterUniversalIdentifiers: viewEntity.viewFilters.map( + ({ universalIdentifier }) => universalIdentifier, + ), + viewGroupUniversalIdentifiers: viewEntity.viewGroups.map( + ({ universalIdentifier }) => universalIdentifier, + ), + viewFilterGroupUniversalIdentifiers: + viewEntity.viewFilterGroups?.map( + ({ universalIdentifier }) => universalIdentifier, + ) ?? [], + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/flat-webhook.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/flat-webhook.module.ts index 1235f45a04f..2374429a2f0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/flat-webhook.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/flat-webhook.module.ts @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceFlatWebhookMapCacheService } from 'src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service'; import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([WebhookEntity]), + TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [WorkspaceFlatWebhookMapCacheService], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service.ts index bfd31872257..cfefa74df7b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/services/workspace-flat-webhook-map-cache.service.ts @@ -5,11 +5,13 @@ import { IsNull, Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatWebhookMaps } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook-maps.type'; import { fromWebhookEntityToFlatWebhook } from 'src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util'; import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -18,19 +20,33 @@ export class WorkspaceFlatWebhookMapCacheService extends WorkspaceCacheProvider< constructor( @InjectRepository(WebhookEntity) private readonly webhookRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } async computeForCache(workspaceId: string): Promise { - const webhooks = await this.webhookRepository.find({ - where: { workspaceId, deletedAt: IsNull() }, - }); + const [webhooks, applications] = await Promise.all([ + this.webhookRepository.find({ + where: { workspaceId, deletedAt: IsNull() }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); const flatWebhookMaps = createEmptyFlatEntityMaps(); for (const webhookEntity of webhooks) { - const flatWebhook = fromWebhookEntityToFlatWebhook(webhookEntity); + const flatWebhook = fromWebhookEntityToFlatWebhook({ + entity: webhookEntity, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatWebhook, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util.ts index 29d89b5d4d9..932f04489ce 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-webhook/utils/from-webhook-entity-to-flat-webhook.util.ts @@ -1,9 +1,26 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type FlatWebhook } from 'src/engine/metadata-modules/flat-webhook/types/flat-webhook.type'; -import { type WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromWebhookEntityToFlatWebhook = ({ + entity: webhookEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'webhook'>): FlatWebhook => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get(webhookEntity.applicationId); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${webhookEntity.applicationId} not found for webhook ${webhookEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromWebhookEntityToFlatWebhook = ( - webhookEntity: WebhookEntity, -): FlatWebhook => { return { id: webhookEntity.id, targetUrl: webhookEntity.targetUrl, @@ -16,5 +33,9 @@ export const fromWebhookEntityToFlatWebhook = ( createdAt: webhookEntity.createdAt.toISOString(), updatedAt: webhookEntity.updatedAt.toISOString(), deletedAt: webhookEntity.deletedAt?.toISOString() ?? null, + __universal: { + universalIdentifier: webhookEntity.universalIdentifier, + applicationUniversalIdentifier, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts index c7e03ab71c1..5a0f5ca9bf3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; @@ -13,7 +14,7 @@ import { FileModule } from 'src/engine/core-modules/file/file.module'; import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module'; import { LogicFunctionTriggerJob } from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job'; import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; @@ -21,16 +22,16 @@ import { LogicFunctionResolver } from 'src/engine/metadata-modules/logic-functio import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service'; import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service'; import { WorkspaceFlatLogicFunctionMapCacheService } from 'src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; -import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; @Module({ imports: [ FileUploadModule, NestjsQueryTypeOrmModule.forFeature([LogicFunctionEntity]), - TypeOrmModule.forFeature([FeatureFlagEntity]), + TypeOrmModule.forFeature([ApplicationEntity, FeatureFlagEntity]), FileModule, ThrottlerModule, ApplicationModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service.ts index 18c6e1c2f15..d8b3b789cba 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service.ts @@ -5,12 +5,14 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; 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 { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; import { fromLogicFunctionEntityToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() @@ -21,6 +23,8 @@ export class WorkspaceFlatLogicFunctionMapCacheService extends WorkspaceCachePro constructor( @InjectRepository(LogicFunctionEntity) private readonly logicFunctionRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, ) { super(); } @@ -28,16 +32,27 @@ export class WorkspaceFlatLogicFunctionMapCacheService extends WorkspaceCachePro async computeForCache( workspaceId: string, ): Promise> { - const logicFunctions = await this.logicFunctionRepository.find({ - where: { workspaceId }, - withDeleted: true, - }); + const [logicFunctions, applications] = await Promise.all([ + this.logicFunctionRepository.find({ + where: { workspaceId }, + withDeleted: true, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); const flatLogicFunctionMaps = createEmptyFlatEntityMaps(); for (const logicFunctionEntity of logicFunctions) { - const flatLogicFunction = - fromLogicFunctionEntityToFlatLogicFunction(logicFunctionEntity); + const flatLogicFunction = fromLogicFunctionEntityToFlatLogicFunction({ + entity: logicFunctionEntity, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatLogicFunction, diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util.ts index cdcc58168a1..0bc5535e77e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/from-logic-function-entity-to-flat-logic-function.util.ts @@ -1,14 +1,32 @@ -import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; -import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util'; import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; +import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type'; + +export const fromLogicFunctionEntityToFlatLogicFunction = ({ + entity: logicFunctionEntity, + applicationIdToUniversalIdentifierMap, +}: FromEntityToFlatEntityArgs<'logicFunction'>): FlatLogicFunction => { + const applicationUniversalIdentifier = + applicationIdToUniversalIdentifierMap.get( + logicFunctionEntity.applicationId, + ); + + if (!isDefined(applicationUniversalIdentifier)) { + throw new FlatEntityMapsException( + `Application with id ${logicFunctionEntity.applicationId} not found for logicFunction ${logicFunctionEntity.id}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } -export const fromLogicFunctionEntityToFlatLogicFunction = ( - logicFunctionEntity: LogicFunctionEntity, -): FlatLogicFunction => { const logicFunctionWithoutRelations = removePropertiesFromRecord( logicFunctionEntity, - ['logicFunctionLayer', 'application'], + getMetadataEntityRelationProperties('logicFunction'), ); return { @@ -16,6 +34,15 @@ export const fromLogicFunctionEntityToFlatLogicFunction = ( createdAt: logicFunctionEntity.createdAt.toISOString(), updatedAt: logicFunctionEntity.updatedAt.toISOString(), deletedAt: logicFunctionEntity.deletedAt?.toISOString() ?? null, - universalIdentifier: logicFunctionEntity.universalIdentifier, + __universal: { + universalIdentifier: logicFunctionEntity.universalIdentifier, + applicationUniversalIdentifier, + publishedVersions: logicFunctionEntity.publishedVersions, + toolInputSchema: logicFunctionEntity.toolInputSchema, + cronTriggerSettings: logicFunctionEntity.cronTriggerSettings, + databaseEventTriggerSettings: + logicFunctionEntity.databaseEventTriggerSettings, + httpRouteTriggerSettings: logicFunctionEntity.httpRouteTriggerSettings, + }, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index b0feaaecce8..72e3ca62321 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -11,7 +11,6 @@ import { UpdateDateColumn, } from 'typeorm'; -import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -21,6 +20,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; @Entity('objectMetadata') @Unique('IDX_OBJECT_METADATA_NAME_SINGULAR_WORKSPACE_ID_UNIQUE', [ @@ -100,6 +100,7 @@ export class ObjectMetadataEntity shortcut: string | null; // TODO: This should not be nullable - legacy field introduced when label identifier was nullable + // TODO: This should be a joinColumn and we should have a FK on this too @Column({ nullable: true, type: 'uuid' }) labelIdentifierFieldMetadataId: string | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index 877455ea99a..5e9d9a5da53 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -34,6 +35,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace @Module({ imports: [ TypeOrmModule.forFeature([ + ApplicationEntity, RoleEntity, RoleTargetEntity, ObjectPermissionEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts index bca8f0804a9..6e0224017f8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; @@ -17,6 +18,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @@ -28,6 +30,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< constructor( @InjectRepository(RoleEntity) private readonly roleRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, @InjectRepository(ObjectPermissionEntity) @@ -49,6 +53,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< ): Promise> { const [ roles, + applications, roleTargets, objectPermissions, permissionFlags, @@ -60,9 +65,14 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< where: { workspaceId }, withDeleted: true, }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + withDeleted: true, + }), this.roleTargetRepository.find({ where: { workspaceId }, - select: ['id', 'roleId'], + select: ['id', 'universalIdentifier', 'roleId'], withDeleted: true, }), this.objectPermissionRepository.find({ @@ -82,12 +92,12 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< }), this.rowLevelPermissionPredicateRepository.find({ where: { workspaceId }, - select: ['id', 'roleId'], + select: ['id', 'universalIdentifier', 'roleId'], withDeleted: true, }), this.rowLevelPermissionPredicateGroupRepository.find({ where: { workspaceId }, - select: ['id', 'roleId'], + select: ['id', 'universalIdentifier', 'roleId'], withDeleted: true, }), ]); @@ -128,20 +138,26 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< ] as const ).map(regroupEntitiesByRelatedEntityId); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + const flatRoleMaps = createEmptyFlatEntityMaps(); for (const roleEntity of roles) { const flatRole = fromRoleEntityToFlatRole({ - ...roleEntity, - roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [], - objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [], - permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [], - fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [], - rowLevelPermissionPredicates: - rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [], - rowLevelPermissionPredicateGroups: - rowLevelPermissionPredicateGroupsByRoleId.get(roleEntity.id) || [], - } as RoleEntity); + entity: { + ...roleEntity, + roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [], + objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [], + permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [], + fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [], + rowLevelPermissionPredicates: + rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [], + rowLevelPermissionPredicateGroups: + rowLevelPermissionPredicateGroupsByRoleId.get(roleEntity.id) || [], + }, + applicationIdToUniversalIdentifierMap, + }); addFlatEntityToFlatEntityMapsThroughMutationOrThrow({ flatEntity: flatRole, diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts index cf937097063..d5f180da968 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; @@ -17,7 +18,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace @Module({ imports: [ - TypeOrmModule.forFeature([WebhookEntity]), + TypeOrmModule.forFeature([ApplicationEntity, WebhookEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationModule, WorkspaceCacheStorageModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts index 6bba08ffde5..6930c70226c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/webhook/webhook.service.ts @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; @@ -16,6 +17,7 @@ import { type CreateWebhookInput } from 'src/engine/metadata-modules/webhook/dto import { type UpdateWebhookInput } from 'src/engine/metadata-modules/webhook/dtos/update-webhook.input'; import { type WebhookDTO } from 'src/engine/metadata-modules/webhook/dtos/webhook.dto'; import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity'; +import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @@ -24,6 +26,8 @@ export class WebhookService { constructor( @InjectRepository(WebhookEntity) private readonly webhookRepository: Repository, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, @@ -40,26 +44,54 @@ export class WebhookService { } async findAll(workspaceId: string): Promise { - const webhooks = await this.webhookRepository.find({ - where: { workspaceId, deletedAt: IsNull() }, - order: { createdAt: 'ASC' }, - }); + const [webhooks, applications] = await Promise.all([ + this.webhookRepository.find({ + where: { workspaceId, deletedAt: IsNull() }, + order: { createdAt: 'ASC' }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); + + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); return webhooks - .map(fromWebhookEntityToFlatWebhook) + .map((webhookEntity) => + fromWebhookEntityToFlatWebhook({ + entity: webhookEntity, + applicationIdToUniversalIdentifierMap, + }), + ) .map(fromFlatWebhookToWebhookDto); } async findById(id: string, workspaceId: string): Promise { - const webhook = await this.webhookRepository.findOne({ - where: { id, workspaceId, deletedAt: IsNull() }, - }); + const [webhook, applications] = await Promise.all([ + this.webhookRepository.findOne({ + where: { id, workspaceId, deletedAt: IsNull() }, + }), + this.applicationRepository.find({ + where: { workspaceId }, + select: ['id', 'universalIdentifier'], + }), + ]); if (!isDefined(webhook)) { return null; } - return fromFlatWebhookToWebhookDto(fromWebhookEntityToFlatWebhook(webhook)); + const applicationIdToUniversalIdentifierMap = + createIdToUniversalIdentifierMap(applications); + + return fromFlatWebhookToWebhookDto( + fromWebhookEntityToFlatWebhook({ + entity: webhook, + applicationIdToUniversalIdentifierMap, + }), + ); } async create( diff --git a/packages/twenty-server/src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type.ts new file mode 100644 index 00000000000..75a3ff19557 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type.ts @@ -0,0 +1,14 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type'; + +export type EntityManyToOneIdByUniversalIdentifierMaps< + T extends AllMetadataName, +> = { + [P in MetadataManyToOneRelatedMetadataNames as `${P}IdToUniversalIdentifierMap`]: Map< + string, + string + >; +} & { + applicationIdToUniversalIdentifierMap: Map; +}; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type.ts new file mode 100644 index 00000000000..5b3f47767bd --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type.ts @@ -0,0 +1,15 @@ +import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type'; +import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; +import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; + +export type EntityWithRegroupedOneToManyRelations< + TEntity extends SyncableEntity, +> = Omit< + TEntity, + ExtractEntityOneToManyEntityRelationProperties +> & { + [P in ExtractEntityOneToManyEntityRelationProperties< + TEntity, + SyncableEntity + >]: RegroupedEntity[]; +}; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type.ts new file mode 100644 index 00000000000..ec08dccefa0 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type.ts @@ -0,0 +1,9 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type'; +import { type EntityManyToOneIdByUniversalIdentifierMaps } from 'src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type'; +import { type EntityWithRegroupedOneToManyRelations } from 'src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type'; + +export type FromEntityToFlatEntityArgs = { + entity: EntityWithRegroupedOneToManyRelations>; +} & EntityManyToOneIdByUniversalIdentifierMaps; diff --git a/packages/twenty-server/src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util.ts b/packages/twenty-server/src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util.ts new file mode 100644 index 00000000000..66e20dbb47e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util.ts @@ -0,0 +1,11 @@ +export const createIdToUniversalIdentifierMap = ( + entities: { id: string; universalIdentifier: string }[], +): Map => { + const map = new Map(); + + for (const entity of entities) { + map.set(entity.id, entity.universalIdentifier); + } + + return map; +}; diff --git a/packages/twenty-server/src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id.ts b/packages/twenty-server/src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id.ts index a23ada1c0fe..3c75700ec25 100644 --- a/packages/twenty-server/src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id.ts +++ b/packages/twenty-server/src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id.ts @@ -4,6 +4,8 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; 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'; +export type RegroupedEntity = { id: string; universalIdentifier: string }; + export type RegroupEntitiesByRelatedEntityIdArgs = MetadataManyToOneJoinColumn extends never ? never @@ -15,7 +17,7 @@ export const regroupEntitiesByRelatedEntityId = ({ entities, foreignKey, }: RegroupEntitiesByRelatedEntityIdArgs) => { - const entitiesByRelatedEntityId = new Map(); + const entitiesByRelatedEntityId = new Map(); for (const entity of entities) { const currentRelatedEntityId = entity[ @@ -30,9 +32,10 @@ export const regroupEntitiesByRelatedEntityId = ({ entitiesByRelatedEntityId.set(currentRelatedEntityId, []); } - entitiesByRelatedEntityId - .get(currentRelatedEntityId)! - .push({ id: entity.id }); + entitiesByRelatedEntityId.get(currentRelatedEntityId)!.push({ + id: entity.id, + universalIdentifier: entity.universalIdentifier, + }); } return entitiesByRelatedEntityId; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts new file mode 100644 index 00000000000..1e722aeb254 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant.ts @@ -0,0 +1,56 @@ +import { type AllMetadataName } from 'twenty-shared/metadata'; + +import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type'; +import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type'; + +export const ALL_JSONB_PROPERTIES_BY_METADATA_NAME = { + fieldMetadata: { + defaultValue: 'defaultValue', + standardOverrides: 'standardOverrides', + options: 'options', + settings: 'settings', + }, + objectMetadata: { + standardOverrides: 'standardOverrides', + duplicateCriteria: 'duplicateCriteria', + }, + view: {}, + viewField: {}, + viewGroup: {}, + viewFilter: { value: 'value' }, + viewFilterGroup: {}, + index: {}, + role: {}, + roleTarget: {}, + rowLevelPermissionPredicate: { value: 'value' }, + rowLevelPermissionPredicateGroup: {}, + logicFunction: { + cronTriggerSettings: 'cronTriggerSettings', + databaseEventTriggerSettings: 'databaseEventTriggerSettings', + httpRouteTriggerSettings: 'httpRouteTriggerSettings', + publishedVersions: 'publishedVersions', + toolInputSchema: 'toolInputSchema', + }, + webhook: {}, + agent: { + responseFormat: 'responseFormat', + modelConfiguration: 'modelConfiguration', + }, + skill: {}, + pageLayout: {}, + pageLayoutTab: {}, + pageLayoutWidget: { + gridPosition: 'gridPosition', + configuration: 'configuration', + }, + commandMenuItem: {}, + navigationMenuItem: {}, + frontComponent: {}, +} as const satisfies { + [P in AllMetadataName]: { + [K in ExtractJsonbProperties>]: K; + }; +}; + +export type AllJsonbPropertiesForMetadataName = + keyof (typeof ALL_JSONB_PROPERTIES_BY_METADATA_NAME)[T]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts index b876d7f906b..9635458d7d2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-record-serialized-relation-properties.test-type.ts @@ -18,24 +18,24 @@ type BrandedObjectWithRelation = JsonbProperty; // eslint-disable-next-line unused-imports/no-unused-vars type ObjectAssertions = [ - // Object with SerializedRelation: Id suffix renamed to UniversalIdentifier + // Object with SerializedRelation: Id suffix renamed to UniversalIdentifier, value becomes nullable Expect< Equal< FormatRecordSerializedRelationProperties, { name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; } > >, - // Branded object with SerializedRelation: renames property, preserves brand key + // Branded object with SerializedRelation: renames property, preserves brand key, value becomes nullable Expect< Equal< FormatRecordSerializedRelationProperties, { name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; __JsonbPropertyBrand__?: undefined; } > @@ -56,6 +56,13 @@ type PrimitiveAssertions = [ Expect, string>>, Expect, number>>, Expect, null>>, + // SerializedRelation becomes nullable + Expect< + Equal< + FormatRecordSerializedRelationProperties, + SerializedRelation | null + > + >, ]; // eslint-disable-next-line unused-imports/no-unused-vars @@ -66,7 +73,7 @@ type ArrayAssertions = [ FormatRecordSerializedRelationProperties, { name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; }[] > >, @@ -88,7 +95,7 @@ type ArrayAssertions = [ FormatRecordSerializedRelationProperties, { name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; }[][] > >, @@ -102,7 +109,7 @@ type UnionAssertions = [ FormatRecordSerializedRelationProperties, { name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; } | null > >, @@ -113,7 +120,7 @@ type UnionAssertions = [ FormatRecordSerializedRelationProperties<(ObjectWithRelation | null)[]>, ({ name: string; - targetFieldMetadataUniversalIdentifier: SerializedRelation; + targetFieldMetadataUniversalIdentifier: SerializedRelation | null; } | null)[] > >, @@ -128,14 +135,14 @@ type MultipleRelationsObject = { // eslint-disable-next-line unused-imports/no-unused-vars type MultipleRelationsAssertions = [ - // Multiple SerializedRelation properties: all get renamed + // Multiple SerializedRelation properties: all get renamed and become nullable Expect< Equal< FormatRecordSerializedRelationProperties, { name: string; - sourceFieldUniversalIdentifier: SerializedRelation; - targetFieldUniversalIdentifier: SerializedRelation; + sourceFieldUniversalIdentifier: SerializedRelation | null; + targetFieldUniversalIdentifier: SerializedRelation | null; regularId: string; } > @@ -190,7 +197,7 @@ type NestedObjectAssertions = [ { name: string; nested: { - fieldMetadataUniversalIdentifier: SerializedRelation; + fieldMetadataUniversalIdentifier: SerializedRelation | null; }; } > @@ -204,7 +211,7 @@ type NestedObjectAssertions = [ name: string; level1: { level2: { - targetUniversalIdentifier: SerializedRelation; + targetUniversalIdentifier: SerializedRelation | null; }; }; } @@ -217,9 +224,9 @@ type NestedObjectAssertions = [ FormatRecordSerializedRelationProperties, { name: string; - directRelationUniversalIdentifier: SerializedRelation; + directRelationUniversalIdentifier: SerializedRelation | null; nested: { - nestedRelationUniversalIdentifier: SerializedRelation; + nestedRelationUniversalIdentifier: SerializedRelation | null; plainField: string; }; } @@ -233,7 +240,7 @@ type NestedObjectAssertions = [ { name: string; nested: { - relationUniversalIdentifier: SerializedRelation; + relationUniversalIdentifier: SerializedRelation | null; } | null; } > @@ -246,9 +253,85 @@ type NestedObjectAssertions = [ { name: string; items: { - itemRelationUniversalIdentifier: SerializedRelation; + itemRelationUniversalIdentifier: SerializedRelation | null; }[]; } > >, ]; + +// JSON Schema-like structures with Record properties +// These should NOT have their 'properties' key renamed +type JsonSchemaLikeObject = { + type: 'object'; + properties: Record; + required?: string[]; +}; + +type JsonSchemaUnion = + | { type: 'text' } + | { + type: 'json'; + schema: JsonSchemaLikeObject; + }; + +type BrandedJsonSchemaUnion = JsonbProperty; + +// eslint-disable-next-line unused-imports/no-unused-vars +type RecordPropertyAssertions = [ + // Object with 'properties' key containing Record: + // The 'properties' key should NOT be renamed to 'propertiesUniversalIdentifier' + Expect< + Equal< + FormatRecordSerializedRelationProperties, + { + type: 'object'; + properties: Record; + required?: string[]; + } + > + >, + + // Union type with JSON schema: properties inside schema should stay as 'properties' + Expect< + Equal< + FormatRecordSerializedRelationProperties, + | { type: 'text' } + | { + type: 'json'; + schema: { + type: 'object'; + properties: Record; + required?: string[]; + }; + } + > + >, + + // Branded JSON schema union: should preserve structure, only add brand + Expect< + Equal< + FormatRecordSerializedRelationProperties, + | { type: 'text'; __JsonbPropertyBrand__?: undefined } + | { + type: 'json'; + schema: { + type: 'object'; + properties: Record; + required?: string[]; + }; + __JsonbPropertyBrand__?: undefined; + } + > + >, + + // Plain Record should pass through unchanged + Expect< + Equal< + FormatRecordSerializedRelationProperties< + Record + >, + Record + > + >, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts index 09bb2ead569..cabed4764d1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts @@ -98,7 +98,10 @@ type NarrowedExpectedResult = { relationType: RelationType; onDelete?: RelationOnDeleteAction | undefined; joinColumnName?: string | null | undefined; - junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined; + junctionTargetFieldUniversalIdentifier?: + | SerializedRelation + | null + | undefined; __JsonbPropertyBrand__?: undefined; }; @@ -111,7 +114,10 @@ type SettingsExpectedResult = relationType: RelationType; onDelete?: RelationOnDeleteAction | undefined; joinColumnName?: string | null | undefined; - junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined; + junctionTargetFieldUniversalIdentifier?: + | SerializedRelation + | null + | undefined; __JsonbPropertyBrand__?: undefined; } | { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts index 62bddcefce3..223885c695a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type.ts @@ -1,17 +1,40 @@ -import { type ExtractSerializedRelationProperties } from 'twenty-shared/types'; +import { + type ExtractSerializedRelationProperties, + type IsSerializedRelation, +} from 'twenty-shared/types'; import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; +// Determines if a property should be transformed to a universal identifier +// A property is transformed only if: +// 1. It matches ExtractSerializedRelationProperties (has the serialized relation brand) +// 2. Its value does NOT have a string index signature (not a Record) +type ShouldTransformToUniversalIdentifier = [P] extends [ + ExtractSerializedRelationProperties, +] + ? string extends keyof NonNullable + ? false + : true + : false; + export type FormatRecordSerializedRelationProperties = T extends unknown ? T extends (infer U)[] ? FormatRecordSerializedRelationProperties[] : T extends string - ? T + ? // By definition we assume that any SerializedRelation are not enforced at pg scope through an FK + // Which mean that SerializedRelation might resolve to non-existent entities ( null ) + IsSerializedRelation extends true + ? T | null + : T : T extends object ? { - [P in keyof T as P extends ExtractSerializedRelationProperties & - string - ? `${RemoveSuffix}UniversalIdentifier` + [P in keyof T as ShouldTransformToUniversalIdentifier< + T, + P + > extends true + ? P extends string + ? `${RemoveSuffix}UniversalIdentifier` + : P : P]: FormatRecordSerializedRelationProperties; } : T diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts index b7a3b835a90..8bc4ce5cbfa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts @@ -1,16 +1,38 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; +import { type AddSuffixToEntityManyToOneProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-many-to-one-properties.type'; +import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type'; import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type'; -import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type'; import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type'; import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type'; import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; -import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type'; +import { type AllJsonbPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant'; import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type'; -import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; -// TODO Handle universal settings +export type UniversalSyncableFlatEntity = Omit< + SyncableEntity, + 'application' | 'applicationId' | 'workspace' | 'workspaceId' +> & { + applicationUniversalIdentifier: string; +}; + +export type UniversalFlatEntityExtraProperties< + TEntity extends SyncableEntity, + TMetadataName extends + AllMetadataName = FromMetadataEntityToMetadataName, +> = AddSuffixToEntityOneToManyProperties & + AddSuffixToEntityManyToOneProperties< + TEntity, + TMetadataName, + 'universalIdentifier' + > & { + applicationUniversalIdentifier: string; + } & { + [P in AllJsonbPropertiesForMetadataName & + keyof TEntity]: FormatRecordSerializedRelationProperties; + }; + export type UniversalFlatEntityFrom< TEntity extends SyncableEntity, TMetadataName extends @@ -24,21 +46,7 @@ export type UniversalFlatEntityFrom< | ExtractEntityRelatedEntityProperties | Extract, keyof TEntity> | keyof CastRecordTypeOrmDatePropertiesToString - | ExtractJsonbProperties + | AllJsonbPropertiesForMetadataName > & - CastRecordTypeOrmDatePropertiesToString & { - [P in ExtractEntityOneToManyEntityRelationProperties< - TEntity, - SyncableEntity - > & - string as `${RemoveSuffix}UniversalIdentifiers`]: string[]; - } & { - [P in Extract, keyof TEntity> & - string as `${RemoveSuffix}UniversalIdentifier`]: TEntity[P]; - } & { - applicationUniversalIdentifier: string; - } & { - [P in ExtractJsonbProperties]: FormatRecordSerializedRelationProperties< - TEntity[P] - >; - }; + CastRecordTypeOrmDatePropertiesToString & + UniversalFlatEntityExtraProperties; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type.ts new file mode 100644 index 00000000000..dfba6e3780a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type.ts @@ -0,0 +1,4 @@ +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type'; + +export type UniversalFlatRole = UniversalFlatEntityFrom; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap index 19ebf944f3a..2d065b31c6a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/__tests__/__snapshots__/flat-entity-deleted-created-updated-matrix-dispatcher.util.spec.ts.snap @@ -1,10 +1,11 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = ` { "createdFlatEntityMaps": { "byId": { "field-id-1": { + "__universal": {}, "applicationId": "application-id-1", "calendarViewIds": [], "createdAt": "2024-01-01T00:00:00.000Z", @@ -69,6 +70,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a dele "deletedFlatEntityMaps": { "byId": { "field-id-1": { + "__universal": {}, "applicationId": "application-id-1", "calendarViewIds": [], "createdAt": "2024-01-01T00:00:00.000Z", @@ -151,6 +153,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "createdFlatEntityMaps": { "byId": { "field-id-3": { + "__universal": {}, "applicationId": "application-id-1", "calendarViewIds": [], "createdAt": "2024-01-01T00:00:00.000Z", @@ -197,6 +200,7 @@ exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect create "deletedFlatEntityMaps": { "byId": { "field-id-2": { + "__universal": {}, "applicationId": "application-id-1", "calendarViewIds": [], "createdAt": "2024-01-01T00:00:00.000Z", diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts index 1bd2034ea9f..0d43bd9da9b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action.ts @@ -18,5 +18,6 @@ export type UpdateFieldAction = export type DeleteFieldAction = BaseDeleteWorkspaceMigrationAction<'fieldMetadata'> & { + // Remove this objectMetadataId: string; }; diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index c8ad67843fa..95e9c137384 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -1,7 +1,5 @@ -import { InjectRepository } from '@nestjs/typeorm'; - import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -11,8 +9,8 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -24,8 +22,7 @@ export class ConnectedAccountDeleteOnePreQueryHook constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceEventEmitter: WorkspaceEventEmitter, - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} async execute( @@ -55,17 +52,18 @@ export class ConnectedAccountDeleteOnePreQueryHook authContext as WorkspaceAuthContext, ); - const objectMetadataEntity = - await this.objectMetadataRepository.findOneOrFail({ - where: { - nameSingular: 'messageChannel', + const { flatObjectMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId: workspace.id, + flatMapsKeys: ['flatObjectMetadataMaps'], }, - relations: ['fields', 'indexMetadatas', 'views'], - }); + ); - const flatObjectMetadata = - fromObjectMetadataEntityToFlatObjectMetadata(objectMetadataEntity); + const flatObjectMetadata = findFlatEntityByUniversalIdentifierOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + universalIdentifier: STANDARD_OBJECT_IDS.messageChannel, + }); // TODO: handle cascade events for delete this.workspaceEventEmitter.emitDatabaseBatchEvent({ diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts index 89d8cdaabaa..3970b4ade2e 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-query-hook.module.ts @@ -2,11 +2,15 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook'; @Module({ - imports: [NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity])], + imports: [ + NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]), + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], providers: [ConnectedAccountDeleteOnePreQueryHook], }) export class ConnectedAccountQueryHookModule {} diff --git a/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts b/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts index feac1e8d3fb..3c24ecfb67a 100644 --- a/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts +++ b/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts @@ -1,14 +1,11 @@ -import { type SERIALIZED_RELATION_BRAND } from './SerializedRelation.type'; - -type HasSerializedRelationPropertyBrand = - typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false; +import { IsSerializedRelation } from "@/types/IsSerializedRelation.type"; export type ExtractSerializedRelationProperties = T extends unknown ? T extends object ? { [P in keyof T]-?: [NonNullable] extends [never] ? never - : HasSerializedRelationPropertyBrand> extends true + : IsSerializedRelation> extends true ? P : never; }[keyof T] diff --git a/packages/twenty-shared/src/types/IsSerializedRelation.type.ts b/packages/twenty-shared/src/types/IsSerializedRelation.type.ts new file mode 100644 index 00000000000..804b6ec461e --- /dev/null +++ b/packages/twenty-shared/src/types/IsSerializedRelation.type.ts @@ -0,0 +1,4 @@ +import { SERIALIZED_RELATION_BRAND } from '@/types/SerializedRelation.type'; + +export type IsSerializedRelation = + typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false; diff --git a/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts b/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts index 58498a0c125..2ccc112af93 100644 --- a/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts +++ b/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts @@ -66,4 +66,20 @@ type Assertions = [ 'rel' > >, + + // Types with string index signatures should return never + // (string index signatures shouldn't be mistaken for the brand) + Expect< + Equal>, never> + >, + Expect< + Equal< + ExtractSerializedRelationProperties< + Record + >, + never + > + >, ]; + + diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index fb0630e72cb..ccabe115f03 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -107,6 +107,7 @@ export type { FromTo } from './FromToType'; export { HTTPMethod } from './HttpMethod'; export type { IsEmptyRecord } from './IsEmptyRecord.type'; export type { IsExactly } from './IsExactly'; +export type { IsSerializedRelation } from './IsSerializedRelation.type'; export type { LogicFunctionEvent } from './LogicFunctionEvent'; export { MessageParticipantRole } from './MessageParticipantRole'; export type { ModifiedProperties } from './ModifiedProperties';