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
@@ -29,6 +29,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@Entity('fieldMetadata')
@Check(
@@ -200,4 +201,13 @@ export class FieldMetadataEntity<
@OneToMany(() => ViewGroupEntity, (viewGroup) => viewGroup.fieldMetadata)
viewGroups: Relation<ViewGroupEntity[]>;
@OneToMany(
() => ViewEntity,
(view) => view.kanbanAggregateOperationFieldMetadata,
)
kanbanAggregateOperationViews: Relation<ViewEntity[]>;
@OneToMany(() => ViewEntity, (view) => view.calendarFieldMetadata)
calendarViews: Relation<ViewEntity[]>;
}
@@ -144,6 +144,7 @@ export class FieldMetadataServiceV2 {
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -154,6 +155,7 @@ export class FieldMetadataServiceV2 {
'flatViewFilterMaps',
'flatViewGroupMaps',
'flatViewMaps',
'flatViewFieldMaps',
],
},
);
@@ -165,6 +167,8 @@ export class FieldMetadataServiceV2 {
updateFieldInput,
flatViewFilterMaps: existingFlatViewFilterMaps,
flatViewGroupMaps: existingFlatViewGroupMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
});
if (inputTranspilationResult.status === 'fail') {
@@ -181,6 +185,9 @@ export class FieldMetadataServiceV2 {
flatViewGroupsToUpdate,
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewFieldsToDelete,
flatViewsToUpdate,
flatViewsToDelete,
} = inputTranspilationResult.result;
const validateAndBuildResult =
@@ -215,6 +222,18 @@ export class FieldMetadataServiceV2 {
flatEntityToDelete: flatViewGroupsToDelete,
flatEntityToUpdate: flatViewGroupsToUpdate,
}),
flatViewMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatViewMaps,
flatEntityToCreate: [],
flatEntityToDelete: flatViewsToDelete,
flatEntityToUpdate: flatViewsToUpdate,
}),
flatViewFieldMaps: computeFlatEntityMapsFromTo({
flatEntityMaps: existingFlatViewFieldMaps,
flatEntityToCreate: [],
flatEntityToDelete: flatViewFieldsToDelete,
flatEntityToUpdate: [],
}),
},
buildOptions: {
isSystemBuild: false,
@@ -222,6 +241,7 @@ export class FieldMetadataServiceV2 {
index: true,
viewGroup: true,
viewFilter: true,
view: true,
},
},
workspaceId,
@@ -365,21 +365,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await queryRunner.commitTransaction();
if (fieldMetadataInput.isActive === false) {
const viewsRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
fieldMetadataInput.workspaceId,
'view',
{
shouldBypassPermissionChecks: true,
},
);
await viewsRepository.delete({
kanbanFieldMetadataId: id,
});
}
if (
updatedFieldMetadata.isActive &&
isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) &&
@@ -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(
@@ -19,8 +19,10 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date;
return {
calendarViewIds: [],
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
viewFieldIds: [],
createdAt,
updatedAt: createdAt,
@@ -32,9 +32,11 @@ export const getRelationTargetFlatFieldMetadataMock = ({
const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date;
return {
calendarViewIds: [],
viewFilterIds: [],
viewGroupIds: [],
viewFieldIds: [],
kanbanAggregateOperationViewIds: [],
createdAt,
updatedAt: createdAt,
description: 'default flat field metadata description',
@@ -105,6 +105,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"flatFieldMetadatas": [
{
"applicationId": null,
"calendarViewIds": [],
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "new field description",
@@ -117,6 +118,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isSystem": false,
"isUIReadOnly": false,
"isUnique": null,
"kanbanAggregateOperationViewIds": [],
"label": "newFieldLabel",
"morphId": Any<String>,
"name": "newFieldPets",
@@ -139,6 +141,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
},
{
"applicationId": null,
"calendarViewIds": [],
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": null,
@@ -151,6 +154,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isSystem": false,
"isUIReadOnly": false,
"isUnique": null,
"kanbanAggregateOperationViewIds": [],
"label": "Pet",
"morphId": null,
"name": "pet",
@@ -175,6 +179,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
},
{
"applicationId": null,
"calendarViewIds": [],
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "new field description",
@@ -187,6 +192,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isSystem": false,
"isUIReadOnly": false,
"isUnique": null,
"kanbanAggregateOperationViewIds": [],
"label": "newFieldLabel",
"morphId": Any<String>,
"name": "newFieldCompanies",
@@ -209,6 +215,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
},
{
"applicationId": null,
"calendarViewIds": [],
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": null,
@@ -221,6 +228,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
"isSystem": false,
"isUIReadOnly": false,
"isUnique": null,
"kanbanAggregateOperationViewIds": [],
"label": "Company",
"morphId": null,
"name": "company",
@@ -48,9 +48,21 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapC
viewGroups: {
id: true,
},
kanbanAggregateOperationViews: {
id: true,
},
calendarViews: {
id: true,
},
},
withDeleted: true,
relations: ['viewFields', 'viewFilters', 'viewGroups'],
relations: [
'viewFields',
'viewFilters',
'viewGroups',
'kanbanAggregateOperationViews',
'calendarViews',
],
});
return fieldMetadatas.reduce(
@@ -12,6 +12,8 @@ export const FIELD_METADATA_RELATION_PROPERTIES = [
'application',
'viewFilters',
'viewGroups',
'kanbanAggregateOperationViews',
'calendarViews',
] as const satisfies (keyof FieldMetadataEntity)[];
export type FieldMetadataEntityRelationProperties =
@@ -23,4 +25,6 @@ export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
viewFieldIds: string[];
viewFilterIds: string[];
viewGroupIds: string[];
kanbanAggregateOperationViewIds: string[];
calendarViewIds: string[];
};
@@ -20,6 +20,9 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
return {
...fieldMetadataWithoutRelations,
kanbanAggregateOperationViewIds:
fieldMetadataEntity.kanbanAggregateOperationViews.map(({ id }) => id),
calendarViewIds: fieldMetadataEntity.calendarViews.map(({ id }) => id),
viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id),
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
@@ -19,6 +19,7 @@ import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
import {
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
type FlatFieldMetadataUpdateSideEffects,
handleFlatFieldMetadataUpdateSideEffect,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util';
@@ -34,6 +35,8 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = {
| 'flatFieldMetadataMaps'
| 'flatViewFilterMaps'
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
>;
type FlatFieldMetadataAndIndexToUpdate = {
@@ -46,6 +49,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
updateFieldInput: rawUpdateFieldInput,
flatViewFilterMaps,
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadataAndIndexToUpdate> => {
const updateFieldInputInformalProperties =
extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [
@@ -114,6 +119,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
return {
status: 'success',
result: {
flatViewsToUpdate: [],
flatViewsToDelete: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatViewGroupsToUpdate: [],
@@ -123,6 +130,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatIndexMetadatasToCreate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewFieldsToDelete: [],
},
};
}
@@ -152,15 +160,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
];
const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = {
...structuredClone(FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS),
flatFieldMetadatasToUpdate: [],
flatIndexMetadatasToUpdate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatIndexMetadatasToCreate: [],
flatIndexMetadatasToDelete: [],
flatViewGroupsToUpdate: [],
};
updatedEditableFieldProperties.options = !isDefined(
@@ -190,6 +191,9 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatViewFiltersToUpdate,
flatIndexMetadatasToCreate,
flatIndexMetadatasToDelete,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
} = handleFlatFieldMetadataUpdateSideEffect({
flatViewFilterMaps,
flatViewGroupMaps,
@@ -198,6 +202,8 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatFieldMetadataMaps,
flatIndexMaps,
toFlatFieldMetadata,
flatViewMaps,
flatViewFieldMaps,
});
return {
@@ -237,6 +243,18 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
...accumulator.flatIndexMetadatasToCreate,
...flatIndexMetadatasToCreate,
],
flatViewsToDelete: [
...accumulator.flatViewsToDelete,
...flatViewsToDelete,
],
flatViewFieldsToDelete: [
...accumulator.flatViewFieldsToDelete,
...flatViewFieldsToDelete,
],
flatViewsToUpdate: [
...accumulator.flatViewsToUpdate,
...flatViewsToUpdate,
],
};
},
initialAccumulator,
@@ -23,6 +23,7 @@ export const getDefaultFlatFieldMetadata = ({
const createdAt = new Date();
return {
calendarViewIds: [],
viewFieldIds: [],
description: createFieldInput.description ?? null,
id: fieldMetadataId,
@@ -57,5 +58,6 @@ export const getDefaultFlatFieldMetadata = ({
applicationId: null,
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
} as const satisfies FlatFieldMetadata;
};
@@ -2,7 +2,6 @@ import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import {
type FlatViewFiltersToDeleteAndUpdate,
recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate,
@@ -15,7 +14,7 @@ import {
import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type';
type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo<
FlatFieldMetadata,
FlatFieldMetadata<EnumFieldMetadataType>,
'flatFieldMetadata'
> &
Pick<AllFlatEntityMaps, 'flatViewFilterMaps' | 'flatViewGroupMaps'>;
@@ -42,13 +41,6 @@ export const handleEnumFlatFieldMetadataUpdateSideEffects = ({
EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT,
);
if (
!isEnumFlatFieldMetadata(toFlatFieldMetadata) ||
!isEnumFlatFieldMetadata(fromFlatFieldMetadata)
) {
return sideEffectResult;
}
if (
JSON.stringify(fromFlatFieldMetadata.options) !==
JSON.stringify(toFlatFieldMetadata.options)
@@ -0,0 +1,90 @@
import { type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
type HandleFlatFieldMetadataDeactivationSideEffectsArgs = FromTo<
FlatFieldMetadata,
'flatFieldMetadata'
> &
Pick<
AllFlatEntityMaps,
| 'flatViewMaps'
| 'flatViewFilterMaps'
| 'flatViewFieldMaps'
| 'flatViewGroupMaps'
>;
export type FieldMetadataDeactivationSideEffect = {
flatViewsToDelete: FlatView[];
flatViewsToUpdate: FlatView[];
flatViewFieldsToDelete: FlatViewField[];
flatViewFiltersToDelete: FlatViewFilter[];
};
export const handleFieldMetadataDeactivationSideEffects = ({
flatViewMaps,
fromFlatFieldMetadata,
flatViewFieldMaps,
flatViewFilterMaps,
flatViewGroupMaps,
}: HandleFlatFieldMetadataDeactivationSideEffectsArgs): FieldMetadataDeactivationSideEffect => {
const flatViewFiltersToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityIds: fromFlatFieldMetadata.viewFilterIds,
flatEntityMaps: flatViewFilterMaps,
},
);
const flatViewFieldsToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: fromFlatFieldMetadata.viewFieldIds,
flatEntityMaps: flatViewFieldMaps,
});
const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: fromFlatFieldMetadata.viewGroupIds,
flatEntityMaps: flatViewGroupMaps,
});
const { flatViewGroupRecordByViewId } = reduceFlatViewGroupsByViewId({
flatViewGroups,
});
// Note: We assume a view only has view groups related to one field
const viewIdsToDelete = [
...new Set([
...Object.keys(flatViewGroupRecordByViewId),
...fromFlatFieldMetadata.calendarViewIds,
]),
];
const flatViewsToDelete = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: viewIdsToDelete,
flatEntityMaps: flatViewMaps,
});
const viewIdsToUpdate =
fromFlatFieldMetadata.kanbanAggregateOperationViewIds.filter(
(viewId) => !viewIdsToDelete.includes(viewId),
);
const flatViewsToUpdate = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityIds: viewIdsToUpdate,
flatEntityMaps: flatViewMaps,
}).map((flatView) => ({
...flatView,
kanbanAggregateOperation: null,
kanbanAggregateOperationFieldMetadataId: null,
}));
return {
flatViewsToUpdate,
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewFiltersToDelete,
};
};
@@ -3,17 +3,23 @@ import { type FromTo } from 'twenty-shared/types';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util';
import {
type FieldMetadataDeactivationSideEffect,
handleFieldMetadataDeactivationSideEffects,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-field-metadata-deactivation-side-effects.util';
import {
type FieldMetadataUpdateIndexSideEffect,
handleIndexChangesDuringFieldUpdate,
} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { type FlatViewFiltersToDeleteAndUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util';
import { type FlatViewGroupsToDeleteUpdateAndCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util';
export type FlatFieldMetadataUpdateSideEffects =
FlatViewFiltersToDeleteAndUpdate &
FlatViewGroupsToDeleteUpdateAndCreate &
FieldMetadataUpdateIndexSideEffect;
FieldMetadataUpdateIndexSideEffect &
FieldMetadataDeactivationSideEffect;
type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
FlatFieldMetadata,
@@ -26,8 +32,25 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
| 'flatFieldMetadataMaps'
| 'flatViewFilterMaps'
| 'flatViewGroupMaps'
| 'flatViewMaps'
| 'flatViewFieldMaps'
>;
export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpdateSideEffects =
{
flatIndexMetadatasToUpdate: [],
flatViewFiltersToDelete: [],
flatViewFiltersToUpdate: [],
flatViewGroupsToCreate: [],
flatViewGroupsToDelete: [],
flatIndexMetadatasToDelete: [],
flatIndexMetadatasToCreate: [],
flatViewGroupsToUpdate: [],
flatViewsToDelete: [],
flatViewFieldsToDelete: [],
flatViewsToUpdate: [],
};
export const handleFlatFieldMetadataUpdateSideEffect = ({
fromFlatFieldMetadata,
toFlatFieldMetadata,
@@ -36,19 +59,59 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatFieldMetadataMaps,
flatViewFilterMaps,
flatViewGroupMaps,
flatViewMaps,
flatViewFieldMaps,
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FlatFieldMetadataUpdateSideEffects => {
const {
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
} = handleEnumFlatFieldMetadataUpdateSideEffects({
flatViewFilterMaps,
flatViewGroupMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
});
const sideEffectResult = structuredClone(
FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS,
);
const isDeactivation =
fromFlatFieldMetadata.isActive === true &&
toFlatFieldMetadata.isActive === false;
if (isDeactivation) {
const {
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewFiltersToDelete,
flatViewsToUpdate,
} = handleFieldMetadataDeactivationSideEffects({
flatViewMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
flatViewFieldMaps,
flatViewFilterMaps,
flatViewGroupMaps,
});
sideEffectResult.flatViewsToUpdate.push(...flatViewsToUpdate);
sideEffectResult.flatViewsToDelete.push(...flatViewsToDelete);
sideEffectResult.flatViewFieldsToDelete.push(...flatViewFieldsToDelete);
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
} else if (
isEnumFlatFieldMetadata(toFlatFieldMetadata) &&
isEnumFlatFieldMetadata(fromFlatFieldMetadata)
) {
const {
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatViewGroupsToUpdate,
} = handleEnumFlatFieldMetadataUpdateSideEffects({
flatViewFilterMaps,
flatViewGroupMaps,
fromFlatFieldMetadata,
toFlatFieldMetadata,
});
sideEffectResult.flatViewFiltersToUpdate.push(...flatViewFiltersToUpdate);
sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate);
sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete);
sideEffectResult.flatViewGroupsToUpdate.push(...flatViewGroupsToUpdate);
sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete);
}
const {
flatIndexMetadatasToUpdate,
@@ -62,14 +125,15 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
flatFieldMetadataMaps,
});
return {
flatIndexMetadatasToUpdate,
flatViewFiltersToDelete,
flatViewFiltersToUpdate,
flatViewGroupsToCreate,
flatViewGroupsToDelete,
flatIndexMetadatasToDelete,
flatIndexMetadatasToCreate,
flatViewGroupsToUpdate,
};
sideEffectResult.flatIndexMetadatasToUpdate.push(
...flatIndexMetadatasToUpdate,
);
sideEffectResult.flatIndexMetadatasToCreate.push(
...flatIndexMetadatasToCreate,
);
sideEffectResult.flatIndexMetadatasToDelete.push(
...flatIndexMetadatasToDelete,
);
return sideEffectResult;
};
@@ -1,4 +1,5 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
@@ -19,6 +20,7 @@ export type ViewEntityRelationProperties =
| ViewGroupEntity
| ViewFilterGroupEntity
| Workspace
| FieldMetadataEntity
>;
export type FlatView = FlatEntityFrom<
@@ -29,6 +29,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: true,
objectMetadataId,
@@ -66,6 +68,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -103,6 +107,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -140,6 +146,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -177,6 +185,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -214,6 +224,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -250,6 +262,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
id: v4(),
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
@@ -286,6 +300,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
type: FieldMetadataType.TS_VECTOR,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
@@ -63,6 +63,8 @@ const generateSourceFlatFieldMetadata = ({
}
return {
calendarViewIds: [],
kanbanAggregateOperationViewIds: [],
viewFilterIds: [],
viewFieldIds: [],
viewGroupIds: [],
@@ -131,7 +133,9 @@ const generateTargetFlatFieldMetadata = ({
return {
morphId: null,
calendarViewIds: [],
viewFieldIds: [],
kanbanAggregateOperationViewIds: [],
viewFilterIds: [],
viewGroupIds: [],
id: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
@@ -250,13 +250,19 @@ export class ViewFieldV2Service {
const {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewMaps: existingFlatViewMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps', 'flatViewMaps'],
},
);
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFieldMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
'flatObjectMetadataMaps',
],
},
);
const existingViewFieldToDelete =
fromDestroyViewFieldInputToFlatViewFieldOrThrow({
@@ -277,6 +283,8 @@ export class ViewFieldV2Service {
},
dependencyAllFlatEntityMaps: {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
@@ -17,6 +17,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/inte
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
@@ -101,6 +102,17 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
@Column({ nullable: true, type: 'uuid' })
kanbanAggregateOperationFieldMetadataId: string | null;
@ManyToOne(
() => FieldMetadataEntity,
(FieldMetadataEntity) => FieldMetadataEntity.kanbanAggregateOperationViews,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'kanbanAggregateOperationFieldMetadataId' })
kanbanAggregateOperationFieldMetadata: Relation<FieldMetadataEntity>;
@Column({
type: 'enum',
enum: Object.values(ViewCalendarLayout),
@@ -112,6 +124,17 @@ export class ViewEntity extends SyncableEntity implements Required<ViewEntity> {
@Column({ nullable: true, type: 'uuid' })
calendarFieldMetadataId: string | null;
@ManyToOne(
() => FieldMetadataEntity,
(fieldMetadata) => fieldMetadata.calendarViews,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'calendarFieldMetadataId' })
calendarFieldMetadata: Relation<FieldMetadataEntity>;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@@ -31,13 +31,20 @@ export class ViewV2Service {
createViewInput: CreateViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatObjectMetadataMaps, flatViewMaps: existingFlatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatViewMaps'],
},
);
const {
flatObjectMetadataMaps,
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectMetadataMaps',
'flatViewMaps',
'flatFieldMetadataMaps',
],
},
);
const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({
createViewInput,
@@ -57,6 +64,7 @@ export class ViewV2Service {
},
dependencyAllFlatEntityMaps: {
flatObjectMetadataMaps: flatObjectMetadataMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
@@ -93,11 +101,14 @@ export class ViewV2Service {
updateViewInput: UpdateViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -118,6 +129,9 @@ export class ViewV2Service {
flatEntityToUpdate: [flatViewFromUpdateInput],
}),
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
},
@@ -153,11 +167,14 @@ export class ViewV2Service {
deleteViewInput: DeleteViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -178,6 +195,9 @@ export class ViewV2Service {
flatEntityToUpdate: [optimisticallyUpdatedFlatViewWithDeletedAt],
}),
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
buildOptions: {
isSystemBuild: false,
},
@@ -213,11 +233,14 @@ export class ViewV2Service {
destroyViewInput: DestroyViewInput;
workspaceId: string;
}): Promise<ViewDTO> {
const { flatViewMaps: existingFlatViewMaps } =
const {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
flatMapsKeys: ['flatViewMaps', 'flatFieldMetadataMaps'],
},
);
@@ -243,6 +266,9 @@ export class ViewV2Service {
view: true,
},
},
dependencyAllFlatEntityMaps: {
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
},
workspaceId,
},
);