Centralized universal identifier validation on create (#18258)

This commit is contained in:
Paul Rastoin
2026-02-26 10:48:24 +01:00
committed by GitHub
parent 2674589b44
commit fde8168a85
4 changed files with 140 additions and 3 deletions
@@ -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<TCode extends string = string> = {
code: TCode;
@@ -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<T>): 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<T>,
): Promise<UniversalFlatEntityValidationReturnType<T, 'create'>> {
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<MetadataFlatEntity<T>>,
errors: uuidValidationResult,
metadataName: this.metadataName,
type: 'create',
};
}
return result;
}
protected abstract validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<T>,
):
@@ -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<String>,
},
"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<String>,
@@ -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);
});