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:
+6
-3
@@ -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);
|
||||
|
||||
|
||||
+122
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+43
@@ -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;
|
||||
};
|
||||
+15
-2
@@ -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;
|
||||
};
|
||||
|
||||
+46
@@ -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],
|
||||
});
|
||||
}
|
||||
};
|
||||
+66
@@ -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],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
+45
-4
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user