Move view field label identifier deletion validation into the cross entity validation (#19642)

## Introduction
In the same validate build and run we should be able to delete a view
field targetting a label identifier and at the same create one that
repoints to it again without failing any validation

Leading for this valdiation rule to be moved in the cross entity
validation steps
This commit is contained in:
Paul Rastoin
2026-04-13 16:53:38 +02:00
committed by prastoin
parent edead399b7
commit 5b4f791ddc
5 changed files with 145 additions and 44 deletions
@@ -0,0 +1,123 @@
import { msg, t } from '@lingui/core/macro';
import { ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { ViewExceptionCode } from 'src/engine/metadata-modules/view/exceptions/view.exception';
import { 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 { 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 { type UniversalDeleteViewFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
export const validateViewFieldLabelIdentifierCrossEntity = ({
optimisticUniversalFlatMaps,
deletedViewFieldActions,
preDeletionFlatViewFieldMaps,
}: {
optimisticUniversalFlatMaps: Pick<
AllUniversalFlatEntityMaps,
'flatObjectMetadataMaps' | 'flatViewMaps' | 'flatViewFieldMaps'
>;
deletedViewFieldActions: UniversalDeleteViewFieldAction[];
preDeletionFlatViewFieldMaps: UniversalFlatEntityMaps<UniversalFlatViewField>;
}): Pick<OrchestratorFailureReport, 'viewField'> => {
const validationErrors: Pick<OrchestratorFailureReport, 'viewField'> = {
viewField: [],
};
if (deletedViewFieldActions.length === 0) {
return validationErrors;
}
const alreadyCheckedViewUniversalIdentifiers = new Set<string>();
for (const deleteAction of deletedViewFieldActions) {
const deletedViewField = findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier: deleteAction.universalIdentifier,
flatEntityMaps: preDeletionFlatViewFieldMaps,
});
const { viewUniversalIdentifier, fieldMetadataUniversalIdentifier } =
deletedViewField;
if (alreadyCheckedViewUniversalIdentifiers.has(viewUniversalIdentifier)) {
continue;
}
const view = findFlatEntityByUniversalIdentifier({
universalIdentifier: viewUniversalIdentifier,
flatEntityMaps: optimisticUniversalFlatMaps.flatViewMaps,
});
if (!isDefined(view) || view.type === ViewType.FIELDS_WIDGET) {
continue;
}
const objectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier: view.objectMetadataUniversalIdentifier,
flatEntityMaps: optimisticUniversalFlatMaps.flatObjectMetadataMaps,
});
if (!isDefined(objectMetadata)) {
continue;
}
const { labelIdentifierFieldMetadataUniversalIdentifier } = objectMetadata;
if (!isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) {
continue;
}
if (
fieldMetadataUniversalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier
) {
continue;
}
alreadyCheckedViewUniversalIdentifiers.add(viewUniversalIdentifier);
const viewFieldUniversalIdentifiers =
view.viewFieldUniversalIdentifiers ?? [];
const hasLabelIdentifierViewField = viewFieldUniversalIdentifiers.some(
(viewFieldUniversalIdentifier) => {
const viewField = findFlatEntityByUniversalIdentifier({
universalIdentifier: viewFieldUniversalIdentifier,
flatEntityMaps: optimisticUniversalFlatMaps.flatViewFieldMaps,
});
return (
isDefined(viewField) &&
viewField.fieldMetadataUniversalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier
);
},
);
if (!hasLabelIdentifierViewField) {
const failedValidation = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: deleteAction.universalIdentifier,
viewUniversalIdentifier,
fieldMetadataUniversalIdentifier,
},
metadataName: 'viewField',
type: 'delete',
});
failedValidation.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
});
validationErrors.viewField.push(failedValidation);
}
}
return validationErrors;
};
@@ -131,6 +131,10 @@ export class WorkspaceMigrationBuildOrchestratorService {
dependencyAllFlatEntityMaps,
});
const preDeletionFlatViewFieldMaps = structuredClone(
optimisticAllFlatEntityMaps.flatViewFieldMaps,
);
const {
flatObjectMetadataMaps,
flatViewFieldMaps,
@@ -818,12 +822,14 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
const { objectMetadata } = crossEntityTransversalValidation({
const { objectMetadata, viewField } = crossEntityTransversalValidation({
optimisticUniversalFlatMaps: optimisticAllFlatEntityMaps,
orchestratorActionsReport,
preDeletionFlatViewFieldMaps,
});
orchestratorFailureReport.objectMetadata.push(...objectMetadata);
orchestratorFailureReport.viewField.push(...viewField);
const allErrors = Object.values(orchestratorFailureReport);
@@ -1,21 +1,32 @@
import { validateObjectMetadataCrossEntity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-cross-entity.util';
import { validateViewFieldLabelIdentifierCrossEntity } from 'src/engine/metadata-modules/flat-view-field/validators/utils/validate-view-field-label-identifier-cross-entity.util';
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 { 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';
export const crossEntityTransversalValidation = ({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
preDeletionFlatViewFieldMaps,
}: {
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
orchestratorActionsReport: OrchestratorActionsReport;
}): Pick<OrchestratorFailureReport, 'objectMetadata'> => {
preDeletionFlatViewFieldMaps: UniversalFlatEntityMaps<UniversalFlatViewField>;
}): Pick<OrchestratorFailureReport, 'objectMetadata' | 'viewField'> => {
const { objectMetadata } = validateObjectMetadataCrossEntity({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
});
return { objectMetadata };
const { viewField } = validateViewFieldLabelIdentifierCrossEntity({
optimisticUniversalFlatMaps,
deletedViewFieldActions: orchestratorActionsReport.viewField.delete,
preDeletionFlatViewFieldMaps,
});
return { objectMetadata, viewField };
};
@@ -127,9 +127,6 @@ export class FlatViewFieldValidatorService {
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatViewFieldMaps: optimisticFlatViewFieldMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatViewMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.viewField
@@ -157,43 +154,6 @@ export class FlatViewFieldValidatorService {
return validationResult;
}
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
existingFlatViewField.fieldMetadataUniversalIdentifier,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(flatFieldMetadata)) {
return validationResult;
}
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatFieldMetadata.objectMetadataUniversalIdentifier,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(flatObjectMetadata)) {
return validationResult;
}
if (
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
existingFlatViewField.fieldMetadataUniversalIdentifier
) {
const flatView = findFlatEntityByUniversalIdentifier({
universalIdentifier: existingFlatViewField.viewUniversalIdentifier,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView) || flatView.type !== ViewType.FIELDS_WIDGET) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
});
}
}
return validationResult;
}
@@ -89,10 +89,11 @@ exports[`View Field Resolver - Successful object metadata identifier update side
},
],
"flatEntityMinimalInformation": {
"fieldMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
"viewUniversalIdentifier": Any<String>,
},
"metadataName": "viewField",
"status": "fail",
"type": "delete",
},
],