From fde8168a85ccd22819a826e0588bdadfc2673044 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:48:24 +0100 Subject: [PATCH] Centralized universal identifier validation on create (#18258) --- .../failed-flat-entity-validation.type.ts | 2 +- ...kspace-entity-migration-builder.service.ts | 51 ++++++++++++++++- ...stall-application.integration-spec.ts.snap | 35 ++++++++++++ ...ng-install-application.integration-spec.ts | 55 ++++++++++++++++++- 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts index 073d09cc2d..d461ab9836 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type.ts @@ -1,8 +1,8 @@ import { type MessageDescriptor } from '@lingui/core'; import { type AllMetadataName } from 'twenty-shared/metadata'; -import { type WorkspaceMigrationActionType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; +import { type WorkspaceMigrationActionType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; export type FlatEntityValidationError = { code: TCode; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts index f6ffd948db..ea14c5caf6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service.ts @@ -3,6 +3,7 @@ import { Inject } from '@nestjs/common'; import { AllMetadataName } from 'twenty-shared/metadata'; import { type FromTo } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; import { @@ -26,6 +27,7 @@ import { resetUniversalFlatEntityForeignKeyAggregators } from 'src/engine/worksp import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/universal-flat-entity-deleted-created-updated-matrix-dispatcher.util'; import { getMetadataEmptyWorkspaceMigrationActionRecord } from 'src/engine/workspace-manager/workspace-migration/utils/get-metadata-empty-workspace-migration-action-record.util'; import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util'; +import { FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type'; import { FailedFlatEntityValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/failed-flat-entity-validate-and-build.type'; import { SuccessfulFlatEntityValidateAndBuild } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/successful-flat-entity-validate-and-build.type'; import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type'; @@ -132,7 +134,7 @@ export abstract class WorkspaceEntityMigrationBuilderService< }, ); - const validationResult = await this.validateFlatEntityCreation({ + const validationResult = await this.innerValidateFlatEntityCreation({ additionalCacheDataMaps, flatEntityToValidate: universalFlatEntityToCreate, workspaceId, @@ -331,6 +333,53 @@ export abstract class WorkspaceEntityMigrationBuilderService< }; } + private validateUniversalIdentifier({ + flatEntityToValidate: { universalIdentifier }, + }: UniversalFlatEntityValidationArgs): FlatEntityValidationError[] { + if ( + !uuidValidate(universalIdentifier) || + uuidVersion(universalIdentifier) !== 4 + ) { + return [ + { + code: FlatEntityMapsExceptionCode.ENTITY_MALFORMED, + message: `Invalid universalIdentifier: "${universalIdentifier}" is not a valid UUID v4`, + value: universalIdentifier, + }, + ]; + } + + return []; + } + + private async innerValidateFlatEntityCreation( + args: UniversalFlatEntityValidationArgs, + ): Promise> { + const uuidValidationResult = this.validateUniversalIdentifier(args); + const result = await this.validateFlatEntityCreation(args); + + if (result.status === 'fail') { + return { + ...result, + errors: [...result.errors, ...uuidValidationResult], + }; + } + + if (result.status === 'success' && uuidValidationResult.length > 0) { + return { + status: 'fail', + flatEntityMinimalInformation: { + universalIdentifier: args.flatEntityToValidate.universalIdentifier, + } as Partial>, + errors: uuidValidationResult, + metadataName: this.metadataName, + type: 'create', + }; + } + + return result; + } + protected abstract validateFlatEntityCreation( args: UniversalFlatEntityValidationArgs, ): diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap index ef57d74298..78c59d83fa 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap @@ -1,5 +1,40 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`Install application should fail when entity does not exist should fail when a role has an invalid universalIdentifier 1`] = ` +{ + "extensions": { + "code": "METADATA_VALIDATION_FAILED", + "errors": { + "role": [ + { + "errors": [ + { + "code": "ENTITY_MALFORMED", + "message": "Invalid universalIdentifier: "not-a-valid-uuid" is not a valid UUID v4", + "value": "not-a-valid-uuid", + }, + ], + "flatEntityMinimalInformation": { + "universalIdentifier": Any, + }, + "metadataName": "role", + "status": "fail", + "type": "create", + }, + ], + }, + "message": "Validation failed for 1 role", + "summary": { + "role": 1, + "totalErrors": 1, + }, + "userFriendlyMessage": "Metadata validation failed", + }, + "message": "Validation errors occurred while syncing application manifest metadata", + "name": "GraphQLError", +} +`; + exports[`Install application should fail when entity does not exist should fail with execution error when deleting non-existent field metadata 1`] = ` { "eventId": Any, diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts index 383549f86e..ff11656d5f 100644 --- a/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts @@ -1,10 +1,20 @@ import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util'; import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util'; +import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util'; +import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util'; +import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util'; import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { v4 as uuidv4 } from 'uuid'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +const INVALID_UUID_APP_ID = uuidv4(); +const INVALID_UUID_ROLE_ID = uuidv4(); + describe('Install application should fail when entity does not exist', () => { + let appCreated = false; + beforeAll(async () => { await updateFeatureFlag({ featureFlag: @@ -12,7 +22,16 @@ describe('Install application should fail when entity does not exist', () => { value: true, expectToFail: false, }); - }); + + await setupApplicationForSync({ + applicationUniversalIdentifier: INVALID_UUID_APP_ID, + name: 'Test Invalid UUID App', + description: 'App for testing UUID v4 validation', + sourcePath: 'test-invalid-uuid', + }); + + appCreated = true; + }, 60000); afterAll(async () => { await updateFeatureFlag({ @@ -23,6 +42,17 @@ describe('Install application should fail when entity does not exist', () => { }); }); + afterEach(async () => { + if (!appCreated) { + return; + } + + await uninstallApplication({ + universalIdentifier: INVALID_UUID_APP_ID, + expectToFail: false, + }); + }); + it('should fail with execution error when deleting non-existent field metadata', async () => { const { errors } = await installApplication({ expectToFail: true, @@ -41,4 +71,27 @@ describe('Install application should fail when entity does not exist', () => { expectOneNotInternalServerErrorSnapshot({ errors }); }); + + it('should fail when a role has an invalid universalIdentifier', async () => { + const manifest = buildBaseManifest({ + appId: INVALID_UUID_APP_ID, + roleId: INVALID_UUID_ROLE_ID, + overrides: { + roles: [ + { + universalIdentifier: 'not-a-valid-uuid', + label: 'Invalid UUID Role', + description: 'Role with invalid universalIdentifier', + }, + ], + }, + }); + + const { errors } = await syncApplication({ + manifest, + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, 60000); });