ViewGroup and ViewFilters side effect in v2 (#15096)

# Introduction

### Summary
Implements side effect handling for `ViewGroup` and `ViewFilters` when
field metadata is updated in the v2 architecture. This ensures that
view-related records are properly maintained when enum field options are
modified, deleted, or created.

### Side effects
- **Side Effect System**: Added side effect handling for field metadata
updates that manages related view groups and view filters
- **Enum Field Updates**: When enum field options are modified, the
system now:
- **View Groups**: Creates new groups for added options, updates
existing groups for modified options, and deletes groups for removed
options
- **View Filters**: Updates filter values to reflect option changes and
removes filters that reference deleted options

### Enum runner fix
Update now works for both atomic enum and array enum ( multi select for
instance )

### Compute flat entity maps from to
Standardized this method usage across v2 services
Next step is to require dependencies dynamically

## Conclusion
closes https://github.com/twentyhq/core-team-issues/issues/1649
This commit is contained in:
Paul Rastoin
2025-10-17 17:09:26 +02:00
committed by GitHub
parent 4f1623ff76
commit 3462a2e288
58 changed files with 3108 additions and 1075 deletions
@@ -59,6 +59,23 @@ export class WorkspaceMigrationV2ViewFilterActionsBuilderService extends Workspa
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewFilterToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const updatedFlatFieldMetadataMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatFieldMetadata,
viewFilterIds: [
...flatFieldMetadata.viewFilterIds,
flatViewFilterToValidate.id,
],
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
return {
status: 'success',
action: {
@@ -66,7 +83,7 @@ export class WorkspaceMigrationV2ViewFilterActionsBuilderService extends Workspa
viewFilter: flatViewFilterToValidate,
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatFieldMetadataMaps: updatedFlatFieldMetadataMaps,
flatViewMaps: updatedFlatViewMaps,
},
};
@@ -59,6 +59,24 @@ export class WorkspaceMigrationV2ViewGroupActionsBuilderService extends Workspac
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatViewMaps,
});
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatViewGroupToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const updatedFlatFieldMetadataMaps =
replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatFieldMetadata,
viewGroupIds: [
...flatFieldMetadata.viewGroupIds,
flatViewGroupToValidate.id,
],
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
return {
status: 'success',
action: {
@@ -66,7 +84,7 @@ export class WorkspaceMigrationV2ViewGroupActionsBuilderService extends Workspac
viewGroup: flatViewGroupToValidate,
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatFieldMetadataMaps: updatedFlatFieldMetadataMaps,
flatViewMaps: updatedFlatViewMaps,
},
};
@@ -112,6 +130,24 @@ export class WorkspaceMigrationV2ViewGroupActionsBuilderService extends Workspac
})
: dependencyOptimisticFlatEntityMaps.flatViewMaps;
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatViewGroupToValidate.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
const updatedFlatFieldMetadataMaps = isDefined(flatFieldMetadata)
? replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: {
...flatFieldMetadata,
viewGroupIds: flatFieldMetadata.viewGroupIds.filter(
(id) => id !== flatViewGroupToValidate.id,
),
},
flatEntityMaps:
dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
})
: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps;
return {
status: 'success',
action: {
@@ -119,7 +155,7 @@ export class WorkspaceMigrationV2ViewGroupActionsBuilderService extends Workspac
viewGroupId: flatViewGroupToValidate.id,
},
dependencyOptimisticFlatEntityMaps: {
...dependencyOptimisticFlatEntityMaps,
flatFieldMetadataMaps: updatedFlatFieldMetadataMaps,
flatViewMaps: updatedFlatViewMaps,
},
};
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { ViewFilterExceptionCode } from 'src/engine/metadata-modules/view-filter/exceptions/view-filter.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
@@ -76,6 +77,7 @@ export class FlatViewFilterValidatorService {
validateFlatViewFilterDeletion({
flatEntityToValidate: flatViewFilterToValidate,
optimisticFlatEntityMaps: optimisticFlatViewFilterMaps,
dependencyOptimisticFlatEntityMaps,
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.viewFilter
>): FailedFlatEntityValidation<FlatViewFilter> {
@@ -98,7 +100,26 @@ export class FlatViewFilterValidatorService {
message: t`View filter not found`,
userFriendlyMessage: msg`View filter not found`,
});
} else if (!isDefined(existingViewFilter.deletedAt)) {
return validationResult;
}
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: existingViewFilter.fieldMetadataId,
flatEntityMaps: dependencyOptimisticFlatEntityMaps.flatFieldMetadataMaps,
});
if (!isDefined(flatFieldMetadata)) {
return validationResult;
}
const shouldValidateDeletion = isEnumFlatFieldMetadata(flatFieldMetadata)
? flatFieldMetadata.options.some(
(option) => option.value === existingViewFilter.value,
)
: true;
if (shouldValidateDeletion && !isDefined(existingViewFilter.deletedAt)) {
validationResult.errors.push({
code: ViewFilterExceptionCode.INVALID_VIEW_FILTER_DATA,
message: t`View filter has to be soft deleted first`,
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-name.constant';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { ViewExceptionCode } from 'src/engine/metadata-modules/view/exceptions/view.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
@@ -88,6 +89,7 @@ export class FlatViewGroupValidatorService {
public validateFlatViewGroupDeletion({
flatEntityToValidate: { id: viewGroupIdToDelete },
optimisticFlatEntityMaps: optimisticFlatViewGroupMaps,
dependencyOptimisticFlatEntityMaps: { flatFieldMetadataMaps },
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.viewGroup
>): FailedFlatEntityValidation<FlatViewGroup> {
@@ -108,14 +110,28 @@ export class FlatViewGroupValidatorService {
message: t`View group to delete not found`,
userFriendlyMessage: msg`View group to delete not found`,
});
} else {
if (!isDefined(existingFlatViewGroup.deletedAt)) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View group to delete has not been soft deleted`,
userFriendlyMessage: msg`View group to delete has not been soft deleted`,
});
}
return validationResult;
}
const relatedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: existingFlatViewGroup.fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (
isDefined(relatedFlatFieldMetadata) &&
isEnumFlatFieldMetadata(relatedFlatFieldMetadata) &&
relatedFlatFieldMetadata.options.some(
(option) => option.value === existingFlatViewGroup.fieldValue,
) &&
!isDefined(existingFlatViewGroup.deletedAt)
) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`View group to delete has not been soft deleted`,
userFriendlyMessage: msg`View group to delete has not been soft deleted`,
});
}
return validationResult;