Validate universalIdentifier uniqueness among application and its dependencies (#19767)

# Introduction
Gracefully validating that when creating an entity its
`universalIdentifier` is available within the all application metadata
maps context ( current app + twenty standard, currently the only managed
dependencies )
This commit is contained in:
Paul Rastoin
2026-04-16 18:59:12 +02:00
committed by GitHub
parent bf410ae438
commit 75235f4621
10 changed files with 610 additions and 9 deletions
@@ -13,6 +13,7 @@ import {
import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report.util';
import { crossEntityTransversalValidation } from 'src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util';
import { mergeOrchestratorFailureReports } from 'src/engine/workspace-manager/workspace-migration/utils/merge-orchestrator-failure-reports.util';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationFieldPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/workspace-migration-field-permission-actions-builder.service';
@@ -822,14 +823,16 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
const { objectMetadata, viewField } = crossEntityTransversalValidation({
const crossEntityFailureReport = crossEntityTransversalValidation({
optimisticUniversalFlatMaps: optimisticAllFlatEntityMaps,
orchestratorActionsReport,
preDeletionFlatViewFieldMaps,
});
orchestratorFailureReport.objectMetadata.push(...objectMetadata);
orchestratorFailureReport.viewField.push(...viewField);
mergeOrchestratorFailureReports({
target: orchestratorFailureReport,
source: crossEntityFailureReport,
});
const allErrors = Object.values(orchestratorFailureReport);
@@ -0,0 +1,122 @@
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { buildAllUniversalIdentifierMap } from 'src/engine/workspace-manager/workspace-migration/utils/build-all-universal-identifier-map.util';
describe('buildAllUniversalIdentifierMap', () => {
it('should return an empty map when all flat entity maps are empty', () => {
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
expect(result.size).toBe(0);
});
it('should collect a single entity from one metadata type', () => {
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
'role-uid-1': {
universalIdentifier: 'role-uid-1',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'role'>,
};
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
expect(result.size).toBe(1);
expect(result.get('role-uid-1')).toEqual({
metadataName: 'role',
applicationUniversalIdentifier: 'app-uid-1',
});
});
it('should collect entities across multiple metadata types', () => {
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
'role-uid-1': {
universalIdentifier: 'role-uid-1',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'role'>,
};
allFlatEntityMaps.flatObjectPermissionMaps.byUniversalIdentifier = {
'perm-uid-1': {
universalIdentifier: 'perm-uid-1',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'objectPermission'>,
};
allFlatEntityMaps.flatViewMaps.byUniversalIdentifier = {
'view-uid-1': {
universalIdentifier: 'view-uid-1',
applicationUniversalIdentifier: 'app-uid-2',
} as MetadataFlatEntity<'view'>,
};
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
expect(result.size).toBe(3);
expect(result.get('role-uid-1')).toEqual({
metadataName: 'role',
applicationUniversalIdentifier: 'app-uid-1',
});
expect(result.get('perm-uid-1')).toEqual({
metadataName: 'objectPermission',
applicationUniversalIdentifier: 'app-uid-1',
});
expect(result.get('view-uid-1')).toEqual({
metadataName: 'view',
applicationUniversalIdentifier: 'app-uid-2',
});
});
it('should skip undefined entries in byUniversalIdentifier', () => {
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
allFlatEntityMaps.flatRoleMaps.byUniversalIdentifier = {
'role-uid-1': undefined,
'role-uid-2': {
universalIdentifier: 'role-uid-2',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'role'>,
};
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
expect(result.size).toBe(1);
expect(result.has('role-uid-1')).toBe(false);
expect(result.get('role-uid-2')).toEqual({
metadataName: 'role',
applicationUniversalIdentifier: 'app-uid-1',
});
});
it('should handle multiple entities within the same metadata type', () => {
const allFlatEntityMaps = createEmptyAllFlatEntityMaps();
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier = {
'field-uid-1': {
universalIdentifier: 'field-uid-1',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'fieldMetadata'>,
'field-uid-2': {
universalIdentifier: 'field-uid-2',
applicationUniversalIdentifier: 'app-uid-1',
} as MetadataFlatEntity<'fieldMetadata'>,
'field-uid-3': {
universalIdentifier: 'field-uid-3',
applicationUniversalIdentifier: 'app-uid-2',
} as MetadataFlatEntity<'fieldMetadata'>,
};
const result = buildAllUniversalIdentifierMap(allFlatEntityMaps);
expect(result.size).toBe(3);
expect(result.get('field-uid-1')?.metadataName).toBe('fieldMetadata');
expect(result.get('field-uid-2')?.metadataName).toBe('fieldMetadata');
expect(result.get('field-uid-3')?.applicationUniversalIdentifier).toBe(
'app-uid-2',
);
});
});
@@ -0,0 +1,43 @@
import {
ALL_METADATA_NAME,
type AllMetadataName,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
export type UniversalIdentifierOwner = {
metadataName: AllMetadataName;
applicationUniversalIdentifier: string;
};
export type AllUniversalIdentifierMap = Map<string, UniversalIdentifierOwner>;
export const buildAllUniversalIdentifierMap = (
allFlatEntityMaps: AllUniversalFlatEntityMaps,
): AllUniversalIdentifierMap => {
const universalIdentifierMap: AllUniversalIdentifierMap = new Map();
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(metadataName);
const flatEntityMaps = allFlatEntityMaps[flatEntityMapsKey];
if (!isDefined(flatEntityMaps)) {
continue;
}
for (const [universalIdentifier, entity] of Object.entries(
flatEntityMaps.byUniversalIdentifier,
)) {
if (isDefined(entity)) {
universalIdentifierMap.set(universalIdentifier, {
metadataName,
applicationUniversalIdentifier: entity.applicationUniversalIdentifier,
});
}
}
}
return universalIdentifierMap;
};
@@ -4,9 +4,11 @@ import {
type OrchestratorActionsReport,
type OrchestratorFailureReport,
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation } from 'src/engine/workspace-manager/workspace-migration/utils/validate-universal-identifier-cross-entity-uniqueness-through-report-mutation.util';
export const crossEntityTransversalValidation = ({
optimisticUniversalFlatMaps,
@@ -16,7 +18,9 @@ export const crossEntityTransversalValidation = ({
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
orchestratorActionsReport: OrchestratorActionsReport;
preDeletionFlatViewFieldMaps: UniversalFlatEntityMaps<UniversalFlatViewField>;
}): Pick<OrchestratorFailureReport, 'objectMetadata' | 'viewField'> => {
}): OrchestratorFailureReport => {
const crossEntityFailureReport = EMPTY_ORCHESTRATOR_FAILURE_REPORT();
const { objectMetadata } = validateObjectMetadataCrossEntity({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
@@ -28,5 +32,14 @@ export const crossEntityTransversalValidation = ({
preDeletionFlatViewFieldMaps,
});
return { objectMetadata, viewField };
crossEntityFailureReport.objectMetadata.push(...objectMetadata);
crossEntityFailureReport.viewField.push(...viewField);
validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
orchestratorFailureReport: crossEntityFailureReport,
});
return crossEntityFailureReport;
};
@@ -0,0 +1,46 @@
import {
ALL_METADATA_NAME,
type AllMetadataName,
} from 'twenty-shared/metadata';
import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
import { type OrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
type AnyFailedValidation = FailedFlatEntityValidation<
AllMetadataName,
WorkspaceMigrationActionType
>;
export const pushToOrchestratorFailureReport = <
TMetadataName extends AllMetadataName,
>({
report,
metadataName,
items,
}: {
report: OrchestratorFailureReport;
metadataName: TMetadataName;
items: FailedFlatEntityValidation<
TMetadataName,
WorkspaceMigrationActionType
>[];
}): void => {
(report[metadataName] as AnyFailedValidation[]).push(...items);
};
export const mergeOrchestratorFailureReports = ({
target,
source,
}: {
target: OrchestratorFailureReport;
source: OrchestratorFailureReport;
}): void => {
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
pushToOrchestratorFailureReport({
report: target,
metadataName,
items: source[metadataName],
});
}
};
@@ -0,0 +1,66 @@
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import {
type OrchestratorActionsReport,
type OrchestratorFailureReport,
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { buildAllUniversalIdentifierMap } from 'src/engine/workspace-manager/workspace-migration/utils/build-all-universal-identifier-map.util';
import { pushToOrchestratorFailureReport } from 'src/engine/workspace-manager/workspace-migration/utils/merge-orchestrator-failure-reports.util';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
export const validateUniversalIdentifierCrossEntityUniquenessThroughReportMutation =
({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
orchestratorFailureReport,
}: {
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
orchestratorActionsReport: OrchestratorActionsReport;
orchestratorFailureReport: OrchestratorFailureReport;
}): void => {
const allUniversalIdentifierMap = buildAllUniversalIdentifierMap(
optimisticUniversalFlatMaps,
);
for (const metadataName of Object.values(ALL_METADATA_NAME)) {
const createActions = orchestratorActionsReport[metadataName]?.create;
if (!isDefined(createActions) || createActions.length === 0) {
continue;
}
for (const createAction of createActions) {
const universalIdentifier = createAction.flatEntity
.universalIdentifier as string;
const existingOwner =
allUniversalIdentifierMap.get(universalIdentifier);
if (!existingOwner || existingOwner.metadataName === metadataName) {
continue;
}
const failedValidation = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName,
type: 'create',
});
failedValidation.errors.push({
code: FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
message: `Cannot create ${metadataName}: universalIdentifier "${universalIdentifier}" is already taken by ${existingOwner.metadataName} from application "${existingOwner.applicationUniversalIdentifier}"`,
});
pushToOrchestratorFailureReport({
report: orchestratorFailureReport,
metadataName,
items: [failedValidation],
});
}
}
};
@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { AllMetadataName } from 'twenty-shared/metadata';
import { type 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';
@@ -18,6 +18,7 @@ import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-e
import { WorkspaceMigrationBuilderAdditionalCacheDataMaps } from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-builder-additional-cache-data-maps.type';
import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
import { UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
import { addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
import { deleteUniversalFlatEntityForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-foreign-key-aggregators.util';
import { deleteUniversalFlatEntityFromUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-and-related-entity-maps-through-mutation-or-throw.util';
@@ -362,26 +363,66 @@ export abstract class WorkspaceEntityMigrationBuilderService<
return [];
}
private validateUniversalIdentifierNotAlreadyInCurrentMetadataMaps({
universalIdentifier,
universalFlatEntityMaps,
}: {
universalFlatEntityMaps: UniversalFlatEntityMaps<
MetadataFlatEntity<typeof this.metadataName>
>;
universalIdentifier: string;
}): FlatEntityValidationError[] {
const existingEntity = findFlatEntityByUniversalIdentifier({
flatEntityMaps: universalFlatEntityMaps,
universalIdentifier,
});
if (isDefined(existingEntity)) {
return [
{
code: FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
message: `Cannot create ${this.metadataName}: universalIdentifier "${universalIdentifier}" already exists in ${this.metadataName} maps from application "${existingEntity.applicationUniversalIdentifier}"`,
},
];
}
return [];
}
private async innerValidateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<T>,
): Promise<UniversalFlatEntityValidationReturnType<T, 'create'>> {
const uuidValidationResult = this.validateUniversalIdentifier(args);
const perTypeExistenceResult =
this.validateUniversalIdentifierNotAlreadyInCurrentMetadataMaps({
universalIdentifier: args.flatEntityToValidate.universalIdentifier,
universalFlatEntityMaps:
args.optimisticFlatEntityMapsAndRelatedFlatEntityMaps[
getMetadataFlatEntityMapsKey(this.metadataName)
],
});
const centralizedErrors = [
...uuidValidationResult,
...perTypeExistenceResult,
];
const result = await this.validateFlatEntityCreation(args);
if (result.status === 'fail') {
return {
...result,
errors: [...result.errors, ...uuidValidationResult],
errors: [...result.errors, ...centralizedErrors],
};
}
if (result.status === 'success' && uuidValidationResult.length > 0) {
if (result.status === 'success' && centralizedErrors.length > 0) {
return {
status: 'fail',
flatEntityMinimalInformation: {
universalIdentifier: args.flatEntityToValidate.universalIdentifier,
} as Partial<MetadataFlatEntity<T>>,
errors: uuidValidationResult,
errors: centralizedErrors,
metadataName: this.metadataName,
type: 'create',
};