From 98e73791a40295b46e6890bb35062d795f9ed152 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:58:38 +0100 Subject: [PATCH] Resolve involved application ids from API metadata (#18221) # Introduction Resolving each create|updated|deleted entities related entities application through their universal foreingKey ( silently failing if not found leaving the validator handling that ) In order to compute all the required application in the dependency flat entity maps ## Tested Creating a field on a custom local twenty-apps installed on the workspace + view field ## Out of scope - updated snapshot - update graphql generated front --- .../src/generated-metadata/graphql.ts | 5 + ...igration-validate-build-and-run-service.ts | 148 +++++++++++++++--- ...enu-item-creation.integration-spec.ts.snap | 4 +- ...enu-item-deletion.integration-spec.ts.snap | 4 +- ...-menu-item-update.integration-spec.ts.snap | 4 +- ...enu-item-creation.integration-spec.ts.snap | 12 +- ...enu-item-deletion.integration-spec.ts.snap | 4 +- ...-menu-item-update.integration-spec.ts.snap | 8 +- ...e-object-metadata.integration-spec.ts.snap | 2 +- ...-predicate-upsert.integration-spec.ts.snap | 8 +- ...er-group-creation.integration-spec.ts.snap | 2 +- 11 files changed, 154 insertions(+), 47 deletions(-) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 346575148a..e2ea3dbac5 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1544,6 +1544,7 @@ export type Field = { settings?: Maybe; standardOverrides?: Maybe; type: FieldMetadataType; + universalIdentifier: Scalars['UUID']; updatedAt: Scalars['DateTime']; }; @@ -1576,6 +1577,7 @@ export type FieldFilter = { isSystem?: InputMaybe; isUIReadOnly?: InputMaybe; or?: InputMaybe>; + universalIdentifier?: InputMaybe; }; /** Type of the field */ @@ -3351,6 +3353,7 @@ export type Object = { nameSingular: Scalars['String']; shortcut?: Maybe; standardOverrides?: Maybe; + universalIdentifier: Scalars['UUID']; updatedAt: Scalars['DateTime']; }; @@ -3400,6 +3403,7 @@ export type ObjectFilter = { isSystem?: InputMaybe; isUIReadOnly?: InputMaybe; or?: InputMaybe>; + universalIdentifier?: InputMaybe; }; export type ObjectIndexMetadatasConnection = { @@ -4597,6 +4601,7 @@ export type UpdateFieldInput = { name?: InputMaybe; options?: InputMaybe; settings?: InputMaybe; + universalIdentifier?: InputMaybe; }; export type UpdateFrontComponentInput = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts index 864b5ab37f..8f0a852b0d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts @@ -6,8 +6,10 @@ import { } from 'twenty-shared/metadata'; import { isDefined } from 'twenty-shared/utils'; +import { FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { MetadataEventEmitter } from 'src/engine/metadata-event-emitter/metadata-event-emitter'; +import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMapsException, @@ -68,6 +70,122 @@ export class WorkspaceMigrationValidateBuildAndRunService { this.isDebugEnabled = logLevels.includes('debug'); } + private computeAllInvolvedApplicationIds({ + allFlatEntityOperationByMetadataName, + flatApplicationMaps, + applicationUniversalIdentifier, + allRelatedFlatEntityMaps, + }: Pick< + ValidateBuildAndRunWorkspaceMigrationFromMatriceArgs, + 'allFlatEntityOperationByMetadataName' | 'applicationUniversalIdentifier' + > & { + flatApplicationMaps: FlatApplicationCacheMaps; + allRelatedFlatEntityMaps: Partial; + }): string[] { + const applicationIds = new Set(); + + const applicationId = + flatApplicationMaps.idByUniversalIdentifier[ + applicationUniversalIdentifier + ]; + + const twentyStandardApplicationId = + flatApplicationMaps.idByUniversalIdentifier[ + TWENTY_STANDARD_APPLICATION.universalIdentifier + ]; + + if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) { + throw new FlatEntityMapsException( + 'Application to build and its dependent application not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + applicationIds.add(applicationId); + + const isBuildingTwentyStandardApplication = + applicationUniversalIdentifier === + TWENTY_STANDARD_APPLICATION.universalIdentifier; + + if (!isBuildingTwentyStandardApplication) { + applicationIds.add(twentyStandardApplicationId); + } + + for (const metadataName of Object.keys( + allFlatEntityOperationByMetadataName, + ) as AllMetadataName[]) { + const flatEntityOperations = + allFlatEntityOperationByMetadataName[metadataName]; + + if (!isDefined(flatEntityOperations)) { + continue; + } + + const { flatEntityToCreate, flatEntityToUpdate, flatEntityToDelete } = + flatEntityOperations; + + const relations = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName]; + + for (const flatEntity of [ + ...flatEntityToCreate, + ...flatEntityToUpdate, + ...flatEntityToDelete, + ]) { + const entityApplicationId = + flatApplicationMaps.idByUniversalIdentifier[ + flatEntity.applicationUniversalIdentifier + ]; + + if (isDefined(entityApplicationId)) { + applicationIds.add(entityApplicationId); + } + + for (const relation of Object.values(relations) as ({ + foreignKey: string; + metadataName: AllMetadataName; + isNullable: boolean; + universalForeignKey: string; + } | null)[]) { + if (!isDefined(relation)) { + continue; + } + + const { universalForeignKey, metadataName: targetMetadataName } = + relation; + + const referencedUniversalIdentifier = + flatEntity[universalForeignKey as keyof typeof flatEntity]; + + if (!isDefined(referencedUniversalIdentifier)) { + continue; + } + + const targetFlatEntityMaps = + allRelatedFlatEntityMaps[ + getMetadataFlatEntityMapsKey( + targetMetadataName as AllMetadataName, + ) + ]; + + if (!isDefined(targetFlatEntityMaps)) { + continue; + } + + const referencedEntity = + targetFlatEntityMaps.byUniversalIdentifier[ + referencedUniversalIdentifier + ]; + + if (isDefined(referencedEntity)) { + applicationIds.add(referencedEntity.applicationId); + } + } + } + } + + return [...applicationIds]; + } + private async computeAllRelatedFlatEntityMaps({ allFlatEntityOperationByMetadataName, workspaceId, @@ -105,26 +223,12 @@ export class WorkspaceMigrationValidateBuildAndRunService { {}, ); - const twentyStandardApplicationId = - flatApplicationMaps.idByUniversalIdentifier[ - TWENTY_STANDARD_APPLICATION.universalIdentifier - ]; - - const applicationId = - flatApplicationMaps.idByUniversalIdentifier[ - applicationUniversalIdentifier - ]; - - if (!isDefined(twentyStandardApplicationId) || !isDefined(applicationId)) { - throw new FlatEntityMapsException( - 'Application to build and its dependent application not found', - FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, - ); - } - - const isBuildingTwentyStandardApplication = - applicationUniversalIdentifier === - TWENTY_STANDARD_APPLICATION.universalIdentifier; + const applicationIds = this.computeAllInvolvedApplicationIds({ + allFlatEntityOperationByMetadataName, + flatApplicationMaps, + applicationUniversalIdentifier, + allRelatedFlatEntityMaps, + }); const dependencyAllFlatEntityMaps = allMetadataNameCacheToCompute.reduce( (allFlatEntityMaps, metadataName) => { @@ -137,9 +241,7 @@ export class WorkspaceMigrationValidateBuildAndRunService { getSubFlatEntityMapsByApplicationIdsOrThrow< MetadataFlatEntity >({ - applicationIds: isBuildingTwentyStandardApplication - ? [applicationId] - : [applicationId, twentyStandardApplicationId], + applicationIds, flatEntityMaps: allRelatedFlatEntityMaps[metadataFlatEntityMapsKey], }), diff --git a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-creation.integration-spec.ts.snap index e7b040dd5f..50fbf11e7a 100644 --- a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-creation.integration-spec.ts.snap @@ -57,7 +57,7 @@ exports[`CommandMenuItem creation should fail when creating with empty workflowV "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -72,7 +72,7 @@ exports[`CommandMenuItem creation should fail when creating with invalid workflo "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-deletion.integration-spec.ts.snap index 68557bb78f..6abadb74d4 100644 --- a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-deletion.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-deletion.integration-spec.ts.snap @@ -36,7 +36,7 @@ exports[`CommandMenuItem deletion should fail when deleting with empty id 1`] = "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -51,7 +51,7 @@ exports[`CommandMenuItem deletion should fail when deleting with invalid id (not "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-update.integration-spec.ts.snap index 076375d804..10d9158cfe 100644 --- a/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/command-menu-item/__snapshots__/failing-command-menu-item-update.integration-spec.ts.snap @@ -22,7 +22,7 @@ exports[`CommandMenuItem update should fail when updating with empty id 1`] = ` "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -72,7 +72,7 @@ exports[`CommandMenuItem update should fail when updating with invalid id (not a "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap index 0f6d137bfb..741ebaf03b 100644 --- a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-creation.integration-spec.ts.snap @@ -10,7 +10,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -25,7 +25,7 @@ exports[`NavigationMenuItem creation should fail when creating with empty target "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -40,7 +40,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid fold "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -55,7 +55,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid targ "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -70,7 +70,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid targ "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -85,7 +85,7 @@ exports[`NavigationMenuItem creation should fail when creating with invalid user "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-deletion.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-deletion.integration-spec.ts.snap index 4c78a9c673..0a46bba97c 100644 --- a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-deletion.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-deletion.integration-spec.ts.snap @@ -22,7 +22,7 @@ exports[`NavigationMenuItem deletion should fail when deleting with empty id 1`] "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -37,7 +37,7 @@ exports[`NavigationMenuItem deletion should fail when deleting with invalid id ( "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update.integration-spec.ts.snap index 8fb0046a7b..6ba2957d47 100644 --- a/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/navigation-menu-item/__snapshots__/failing-navigation-menu-item-update.integration-spec.ts.snap @@ -22,7 +22,7 @@ exports[`NavigationMenuItem update should fail when updating with empty id 1`] = "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -37,7 +37,7 @@ exports[`NavigationMenuItem update should fail when updating with invalid folder "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -52,7 +52,7 @@ exports[`NavigationMenuItem update should fail when updating with invalid id (no "userFriendlyMessage": "An error occurred.", "value": "not-a-valid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -67,7 +67,7 @@ exports[`NavigationMenuItem update should fail when updating with missing id 1`] "userFriendlyMessage": "An error occurred.", "value": "", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index 06a48f511c..8db0da5565 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -65,7 +65,7 @@ exports[`Object metadata update should fail when labelIdentifier is not a uuid 1 "userFriendlyMessage": "An error occurred.", "value": "not-a-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", }, ] diff --git a/packages/twenty-server/test/integration/metadata/suites/row-level-permission-predicate/__snapshots__/failing-row-level-permission-predicate-upsert.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/row-level-permission-predicate/__snapshots__/failing-row-level-permission-predicate-upsert.integration-spec.ts.snap index b416b938d0..38f1ff20bb 100644 --- a/packages/twenty-server/test/integration/metadata/suites/row-level-permission-predicate/__snapshots__/failing-row-level-permission-predicate-upsert.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/row-level-permission-predicate/__snapshots__/failing-row-level-permission-predicate-upsert.integration-spec.ts.snap @@ -22,7 +22,7 @@ exports[`Row Level Permission Predicate upsert should fail when fieldMetadataId "userFriendlyMessage": "An error occurred.", "value": "invalid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -49,7 +49,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId "userFriendlyMessage": "An error occurred.", "value": "invalid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -64,7 +64,7 @@ exports[`Row Level Permission Predicate upsert should fail when objectMetadataId "userFriendlyMessage": "An error occurred.", "value": "invalid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; @@ -91,7 +91,7 @@ exports[`Row Level Permission Predicate upsert should fail when roleId is not a "userFriendlyMessage": "An error occurred.", "value": "invalid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/view-filter-group/__snapshots__/failing-view-filter-group-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/view-filter-group/__snapshots__/failing-view-filter-group-creation.integration-spec.ts.snap index 1f97f5625f..860fbad3e9 100644 --- a/packages/twenty-server/test/integration/metadata/suites/view-filter-group/__snapshots__/failing-view-filter-group-creation.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/view-filter-group/__snapshots__/failing-view-filter-group-creation.integration-spec.ts.snap @@ -46,7 +46,7 @@ exports[`View Filter Group creation should fail when viewId is not a valid UUID "userFriendlyMessage": "An error occurred.", "value": "invalid-uuid", }, - "message": "Invalid UUID", + "message": "Invalid UUID: 'not-a-valid-uuid'", "name": "ValidationError", } `;