diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/validators/utils/validate-view-field-label-identifier-cross-entity.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/validators/utils/validate-view-field-label-identifier-cross-entity.util.ts new file mode 100644 index 00000000000..e148229f8a5 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/validators/utils/validate-view-field-label-identifier-cross-entity.util.ts @@ -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; +}): Pick => { + const validationErrors: Pick = { + viewField: [], + }; + + if (deletedViewFieldActions.length === 0) { + return validationErrors; + } + + const alreadyCheckedViewUniversalIdentifiers = new Set(); + + 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; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index 01b97a4a997..579cabc4cae 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -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); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts index 3899200f233..37b7d32015e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/utils/cross-entity-transversal-validation.util.ts @@ -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 => { + preDeletionFlatViewFieldMaps: UniversalFlatEntityMaps; +}): Pick => { const { objectMetadata } = validateObjectMetadataCrossEntity({ optimisticUniversalFlatMaps, orchestratorActionsReport, }); - return { objectMetadata }; + const { viewField } = validateViewFieldLabelIdentifierCrossEntity({ + optimisticUniversalFlatMaps, + deletedViewFieldActions: orchestratorActionsReport.viewField.delete, + preDeletionFlatViewFieldMaps, + }); + + return { objectMetadata, viewField }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service.ts index 34b1a21807b..02e9a0ca27d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-field-validator.service.ts @@ -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; } diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap index 18dc91499f3..8c13edaf4af 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field/__snapshots__/object-identifier-update-side-effect-on-view-field.integration-spec.ts.snap @@ -89,10 +89,11 @@ exports[`View Field Resolver - Successful object metadata identifier update side }, ], "flatEntityMinimalInformation": { + "fieldMetadataUniversalIdentifier": Any, "universalIdentifier": Any, + "viewUniversalIdentifier": Any, }, "metadataName": "viewField", - "status": "fail", "type": "delete", }, ],