Field deactivation side effect views calendar kanban viewFields (#15180)

# Introduction
Handling both:
- field deactivation side effect on view fields, view filters and views
- field deactivation side effect on view that targets it as
`kanbanAggregateFieldMetadataId`
- field deactivation side effect on view that targets it as
`calendarFieldMetadataId`

## Coverage
added coverage
```ts
 PASS  test/integration/metadata/suites/field-metadata/kanban-aggregate-field-deactivation-deletes-views.integration-spec.ts (13.132 s)
  kanban-aggregate-field-deactivation-nullifies-kanban-properties
    ✓ should nullify kanban properties when field used as kanbanAggregateOperationFieldMetadataId is deactivated (3923 ms)
    ✓ should not modify views when field not used as kanbanAggregateOperationFieldMetadataId is deactivated (2958 ms)
    ✓ should nullify kanban properties on multiple views when they all use the same field as kanbanAggregateOperationFieldMetadataId (2542 ms)
    ✓ should nullify kanban properties when views have different aggregate operations on same field (3380 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        13.154 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/view-group-field-deactivation-deletes-views.integration-spec.ts (12.639 s)
  view-group-field-deactivation-deletes-views
    ✓ should delete view when field used in view group is deactivated (3469 ms)
    ✓ should not delete view when field not used in view group is deactivated (3109 ms)
    ✓ should delete multiple views when they all use the same field in view groups (2741 ms)
    ✓ should handle deactivation when view has multiple view groups with different fields (3008 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        12.664 s
```

```ts
 PASS  test/integration/metadata/suites/field-metadata/calendar-field-deactivation-deletes-views.integration-spec.ts (14.579 s)
  calendar-field-deactivation-deletes-views
    ✓ should delete view when field used as calendarFieldMetadataId is deactivated (3388 ms)
    ✓ should not delete view when field not used as calendarFieldMetadataId is deactivated (2438 ms)
    ✓ should delete multiple views when they all use the same field as calendarFieldMetadataId (2635 ms)
    ✓ should handle deactivation when views have different calendar layouts on same field (3195 ms)
    ✓ should delete calendar view but not other view types when calendar field is deactivated (2682 ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        14.601 s, estimated 15 s
```

## View soft deletion
We decided to remove the soft deletion grain on all the views, in this
PR context we've only removed soft deleted validation requirement on any
view entities

## Conclusion

close https://github.com/twentyhq/core-team-issues/issues/1754
This commit is contained in:
Paul Rastoin
2025-10-21 16:12:03 +02:00
committed by GitHub
parent 72fd8ae8d2
commit 45473218d3
45 changed files with 1674 additions and 194 deletions
@@ -3,64 +3,54 @@ import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
type PropertyNameToRelationName<T extends string> = T extends `${infer Name}Id`
? Name
: never;
type ExtractEntityRelations<TEntity extends MetadataEntity<AllMetadataName>> = {
[K in ExtractPropertiesThatEndsWithId<
TEntity,
'id' | 'workspaceId'
> as PropertyNameToRelationName<K>]: K;
[K in ExtractPropertiesThatEndsWithId<TEntity, 'id' | 'workspaceId'>]: K;
};
type MetadataRelatedMetadataNames<T extends AllMetadataName> = Extract<
keyof ExtractEntityRelations<MetadataEntity<T>>,
AllMetadataName
>;
type MetadataRelatedMetadataNames<T extends AllMetadataName> =
keyof ExtractEntityRelations<MetadataEntity<T>>;
type MetadataNameAndRelations = {
[T in AllMetadataName]: MetadataRelatedMetadataNames<T> extends never
? Record<string, never>
: Record<MetadataRelatedMetadataNames<T>, string> & {
[K in Exclude<
AllMetadataName,
MetadataRelatedMetadataNames<T>
>]?: string;
: {
[P in MetadataRelatedMetadataNames<T>]?: AllMetadataName;
};
};
export const ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS = {
export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
fieldMetadata: {
objectMetadata: 'objectMetadataId',
objectMetadataId: 'objectMetadata',
},
objectMetadata: {},
view: {
objectMetadata: 'objectMetadataId',
kanbanAggregateOperationFieldMetadataId: 'fieldMetadata',
calendarFieldMetadataId: 'fieldMetadata',
objectMetadataId: 'objectMetadata',
},
viewField: {
view: 'viewId',
fieldMetadata: 'fieldMetadataId',
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
},
viewGroup: {
view: 'viewId',
fieldMetadata: 'fieldMetadataId',
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
},
index: {
objectMetadata: 'objectMetadataId',
objectMetadataId: 'objectMetadata',
},
serverlessFunction: {},
cronTrigger: {
serverlessFunction: 'serverlessFunctionId',
serverlessFunctionId: 'serverlessFunction',
},
databaseEventTrigger: {
serverlessFunction: 'serverlessFunctionId',
serverlessFunctionId: 'serverlessFunction',
},
routeTrigger: {
serverlessFunction: 'serverlessFunctionId',
serverlessFunctionId: 'serverlessFunction',
},
viewFilter: {
view: 'viewId',
fieldMetadata: 'fieldMetadataId',
viewId: 'view',
fieldMetadataId: 'fieldMetadata',
},
} as const satisfies MetadataNameAndRelations;
@@ -18,6 +18,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
fieldMetadata: true,
},
view: {
fieldMetadata: true,
objectMetadata: true,
},
viewField: {
@@ -1,8 +1,8 @@
import { type ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { type ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type';
export type MetadataManyToOneRelatedMetadataNames<T extends AllMetadataName> =
Extract<
keyof (typeof ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS)[T],
(typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T][keyof (typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T]],
AllMetadataName
>;
@@ -1,4 +1,4 @@
import { ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
@@ -32,8 +32,9 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsOrThrow = <
flatEntityMaps: initialFlatEntityAndRelatedMaps[flatEntityMapsKey],
});
// TODO refactor
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_NAME_MANY_TO_ONE_RELATIONS[metadataName],
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
);
return manyToOneRelatedMetadataName.reduce(