diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts index dbab55f3a0..a4282937b3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts @@ -13,11 +13,8 @@ import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/ import { fromCreateCronTriggerInputToFlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/utils/from-create-cron-trigger-input-to-flat-cron-trigger.util'; import { fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/cron-trigger/utils/from-update-cron-trigger-input-to-flat-cron-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @@ -49,20 +46,17 @@ export class CronTriggerV2Service { }, ); - const toFlatCronTriggerMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatCronTriggerToCreate, - flatEntityMaps: existingFlatCronTriggerMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatCronTriggerMaps: { - from: existingFlatCronTriggerMaps, - to: toFlatCronTriggerMaps, - }, + flatCronTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatCronTriggerMaps, + flatEntityToCreate: [flatCronTriggerToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -115,24 +109,17 @@ export class CronTriggerV2Service { updateCronTriggerInput: cronTriggerInput, }); - const fromFlatCronTriggerMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [optimisticallyUpdatedFlatCronTrigger.id], - flatEntityMaps: existingFlatCronTriggerMaps, - }); - const toFlatCronTriggerMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatCronTrigger, - flatEntityMaps: fromFlatCronTriggerMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatCronTriggerMaps: { - from: existingFlatCronTriggerMaps, - to: toFlatCronTriggerMaps, - }, + flatCronTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatCronTriggerMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatCronTrigger], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -193,23 +180,16 @@ export class CronTriggerV2Service { ); } - const fromFlatCronTriggerMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingFlatCronTrigger.id], - flatEntityMaps: existingFlatCronTriggerMaps, - }); - const toFlatCronTriggerMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatCronTriggerMaps, - entityToDeleteId: existingFlatCronTrigger.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatCronTriggerMaps: { - from: fromFlatCronTriggerMaps, - to: toFlatCronTriggerMaps, - }, + flatCronTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatCronTriggerMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatCronTrigger], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts index 6bad026633..8ed046b4d5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts @@ -13,11 +13,8 @@ import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-e import { fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/utils/from-create-database-event-trigger-input-to-flat-database-event-trigger.util'; import { fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/database-event-trigger/utils/from-update-database-event-trigger-input-to-flat-database-event-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @@ -52,22 +49,17 @@ export class DatabaseEventTriggerV2Service { workspaceId, }); - const toFlatDatabaseEventTriggerMaps = addFlatEntityToFlatEntityMapsOrThrow( - { - flatEntity: flatDatabaseEventTriggerToCreate, - flatEntityMaps: existingFlatDatabaseEventTriggerMaps, - }, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatDatabaseEventTriggerMaps: { - from: existingFlatDatabaseEventTriggerMaps, - to: toFlatDatabaseEventTriggerMaps, - }, + flatDatabaseEventTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatDatabaseEventTriggerMaps, + flatEntityToCreate: [flatDatabaseEventTriggerToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -129,25 +121,19 @@ export class DatabaseEventTriggerV2Service { }, ); - const fromFlatDatabaseEventTriggerMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [optimisticallyUpdatedFlatDatabaseEventTrigger.id], - flatEntityMaps: existingFlatDatabaseEventTriggerMaps, - }); - const toFlatDatabaseEventTriggerMaps = - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatDatabaseEventTrigger, - flatEntityMaps: fromFlatDatabaseEventTriggerMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatDatabaseEventTriggerMaps: { - from: existingFlatDatabaseEventTriggerMaps, - to: toFlatDatabaseEventTriggerMaps, - }, + flatDatabaseEventTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatDatabaseEventTriggerMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatDatabaseEventTrigger, + ], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -216,24 +202,16 @@ export class DatabaseEventTriggerV2Service { ); } - const fromFlatDatabaseEventTriggerMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingFlatDatabaseEventTrigger.id], - flatEntityMaps: existingFlatDatabaseEventTriggerMaps, - }); - const toFlatDatabaseEventTriggerMaps = - deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatDatabaseEventTriggerMaps, - entityToDeleteId: existingFlatDatabaseEventTrigger.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatDatabaseEventTriggerMaps: { - from: fromFlatDatabaseEventTriggerMaps, - to: toFlatDatabaseEventTriggerMaps, - }, + flatDatabaseEventTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatDatabaseEventTriggerMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatDatabaseEventTrigger], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 197b61b903..840b855150 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -26,6 +26,8 @@ import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-meta import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; 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'; @Entity('fieldMetadata') @Check( @@ -190,4 +192,10 @@ export class FieldMetadataEntity< @OneToMany(() => ViewFieldEntity, (viewField) => viewField.fieldMetadata) viewFields: Relation; + + @OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.fieldMetadata) + viewFilters: Relation; + + @OneToMany(() => ViewGroupEntity, (viewGroup) => viewGroup.fieldMetadata) + viewGroups: Relation; } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 037e5f55d2..fdaa187af3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -13,12 +13,8 @@ import { FieldMetadataException, FieldMetadataExceptionCode, } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util'; import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; @@ -91,45 +87,6 @@ export class FieldMetadataServiceV2 { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); - const fromFlatFieldMetadataMaps = getSubFlatEntityMapsOrThrow({ - flatEntityMaps: existingFlatFieldMetadataMaps, - flatEntityIds: [ - ...new Set(flatFieldMetadatasToDelete.map((flatField) => flatField.id)), - ], - }); - - const toFlatFieldMetadataMaps = flatFieldMetadatasToDelete.reduce( - (flatFieldMetadataMaps, flatFieldMetadataToDelete) => - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatFieldMetadataToDelete.id, - flatEntityMaps: flatFieldMetadataMaps, - }), - fromFlatFieldMetadataMaps, - ); - - const fromFlatIndexMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [...flatIndexesToUpdate, ...flatIndexesToDelete].map( - ({ id }) => id, - ), - flatEntityMaps: existingFlatIndexMaps, - }); - const toFlatIndexMapsWithUpdatedFlatIndex = flatIndexesToUpdate.reduce( - (flatIndexMaps, flatIndex) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatIndex, - flatEntityMaps: flatIndexMaps, - }), - fromFlatIndexMaps, - ); - const toFlatIndexMaps = flatIndexesToDelete.reduce( - (flatIndexMaps, flatIndex) => - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatIndex.id, - flatEntityMaps: flatIndexMaps, - }), - toFlatIndexMapsWithUpdatedFlatIndex, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -144,14 +101,18 @@ export class FieldMetadataServiceV2 { }, }, fromToAllFlatEntityMaps: { - flatFieldMetadataMaps: { - from: fromFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, - flatIndexMaps: { - from: fromFlatIndexMaps, - to: toFlatIndexMaps, - }, + flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: flatFieldMetadatasToDelete, + flatEntityToUpdate: [], + }), + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: [], + flatEntityToDelete: flatIndexesToDelete, + flatEntityToUpdate: flatIndexesToUpdate, + }), }, workspaceId, }, @@ -180,6 +141,9 @@ export class FieldMetadataServiceV2 { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatIndexMaps: existingFlatIndexMaps, flatFieldMetadataMaps: existingFlatFieldMetadataMaps, + flatViewFilterMaps: existingFlatViewFilterMaps, + flatViewGroupMaps: existingFlatViewGroupMaps, + flatViewMaps: existingFlatViewMaps, } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, @@ -187,6 +151,9 @@ export class FieldMetadataServiceV2 { 'flatObjectMetadataMaps', 'flatIndexMaps', 'flatFieldMetadataMaps', + 'flatViewFilterMaps', + 'flatViewGroupMaps', + 'flatViewMaps', ], }, ); @@ -196,6 +163,8 @@ export class FieldMetadataServiceV2 { flatIndexMaps: existingFlatIndexMaps, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, updateFieldInput, + flatViewFilterMaps: existingFlatViewFilterMaps, + flatViewGroupMaps: existingFlatViewGroupMaps, }); if (inputTranspilationResult.status === 'fail') { @@ -203,81 +172,56 @@ export class FieldMetadataServiceV2 { } const { - flatFieldMetadatasToUpdate: optimisticallyUpdatedFlatFieldMetadatas, + flatFieldMetadatasToUpdate, flatIndexMetadatasToUpdate, flatIndexMetadatasToDelete, flatIndexMetadatasToCreate, + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatViewGroupsToUpdate, + flatViewFiltersToDelete, + flatViewFiltersToUpdate, } = inputTranspilationResult.result; - const toFlatFieldMetadataMaps = - optimisticallyUpdatedFlatFieldMetadatas.reduce( - (flatFieldMaps, flatFieldMetadata) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntityMaps: flatFieldMaps, - flatEntity: flatFieldMetadata, - }), - getSubFlatEntityMapsOrThrow({ - flatEntityIds: optimisticallyUpdatedFlatFieldMetadatas.map( - ({ id }) => id, - ), - flatEntityMaps: existingFlatFieldMetadataMaps, - }), - ); - - const fromFlatIndexMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [ - ...flatIndexMetadatasToUpdate, - ...flatIndexMetadatasToDelete, - ].map(({ id }) => id), - flatEntityMaps: existingFlatIndexMaps, - }); - const toFlatIndexMapsWithCreatedFlatIndex = - flatIndexMetadatasToCreate.reduce( - (flatIndexMaps, flatIndex) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatIndex, - flatEntityMaps: flatIndexMaps, - }), - fromFlatIndexMaps, - ); - const toFlatIndexMapsWithUpdatedFlatIndex = - flatIndexMetadatasToUpdate.reduce( - (flatIndexMaps, flatIndex) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatIndex, - flatEntityMaps: flatIndexMaps, - }), - toFlatIndexMapsWithCreatedFlatIndex, - ); - const toFlatIndexMaps = flatIndexMetadatasToDelete.reduce( - (flatIndexMaps, flatIndex) => - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatIndex.id, - flatEntityMaps: flatIndexMaps, - }), - toFlatIndexMapsWithUpdatedFlatIndex, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { dependencyAllFlatEntityMaps: { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, + flatViewMaps: existingFlatViewMaps, }, fromToAllFlatEntityMaps: { - flatFieldMetadataMaps: { - from: existingFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, - flatIndexMaps: { - from: fromFlatIndexMaps, - to: toFlatIndexMaps, - }, + flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: flatFieldMetadatasToUpdate, + }), + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: flatIndexMetadatasToCreate, + flatEntityToDelete: flatIndexMetadatasToDelete, + flatEntityToUpdate: flatIndexMetadatasToUpdate, + }), + flatViewFilterMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFilterMaps, + flatEntityToCreate: [], + flatEntityToDelete: flatViewFiltersToDelete, + flatEntityToUpdate: flatViewFiltersToUpdate, + }), + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: flatViewGroupsToCreate, + flatEntityToDelete: flatViewGroupsToDelete, + flatEntityToUpdate: flatViewGroupsToUpdate, + }), }, buildOptions: { isSystemBuild: false, inferDeletionFromMissingEntities: { index: true, + viewGroup: true, + viewFilter: true, }, }, workspaceId, @@ -293,7 +237,7 @@ export class FieldMetadataServiceV2 { return this.fieldMetadataRepository.findOneOrFail({ where: { - id: optimisticallyUpdatedFlatFieldMetadatas[0].id, + id: flatFieldMetadatasToUpdate[0].id, workspaceId, }, }); @@ -358,24 +302,6 @@ export class FieldMetadataServiceV2 { { flatFieldMetadatas: [], indexMetadatas: [] }, ); - const toFlatFieldMetadataMaps = flatFieldMetadatasToCreate.reduce( - (flatFieldMaps, flatFieldMetadataToCreate) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatFieldMetadataToCreate, - flatEntityMaps: flatFieldMaps, - }), - EMPTY_FLAT_ENTITY_MAPS, - ); - - const toFlatIndexMaps = flatIndexMetadatasToCreate.reduce( - (flatIndexMaps, flatIndex) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatIndex, - flatEntityMaps: flatIndexMaps, - }), - EMPTY_FLAT_ENTITY_MAPS, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { @@ -383,14 +309,18 @@ export class FieldMetadataServiceV2 { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }, fromToAllFlatEntityMaps: { - flatFieldMetadataMaps: { - from: existingFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, - flatIndexMaps: { - from: existingFlatIndexMaps, - to: toFlatIndexMaps, - }, + flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: flatFieldMetadatasToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: flatIndexMetadatasToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, buildOptions: { isSystemBuild: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type.ts index 2c02373bdf..91c603c7ab 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type.ts @@ -1,11 +1,9 @@ import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type'; import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type'; +import { type FlatEntityPropertyUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; -import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; export type FlatEntityPropertiesUpdates< T extends AllMetadataName, K extends FlatEntityPropertiesToCompare = FlatEntityPropertiesToCompare, -> = Array< - PropertyUpdate, Extract>> ->; +> = Array>>>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type.ts new file mode 100644 index 0000000000..c9e61c1a79 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/flat-entity-property-update.type.ts @@ -0,0 +1,12 @@ +import { type AllMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-metadata-name.type'; +import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type'; +import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; +import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; + +export type FlatEntityPropertyUpdate< + T extends AllMetadataName, + K extends FlatEntityPropertiesToCompare = FlatEntityPropertiesToCompare, +> = PropertyUpdate< + MetadataFlatEntity, + Extract> +>; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util.ts new file mode 100644 index 0000000000..40a8fed5ce --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util.ts @@ -0,0 +1,64 @@ +import { type AllFlatEntities } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entities.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; + +type ComputeFlatEntityMapsFromToArgs = { + flatEntityMaps: FlatEntityMaps; + flatEntityToUpdate: T[]; + flatEntityToCreate: T[]; + flatEntityToDelete: T[]; +}; +export const computeFlatEntityMapsFromTo = ({ + flatEntityMaps, + flatEntityToCreate, + flatEntityToDelete, + flatEntityToUpdate, +}: ComputeFlatEntityMapsFromToArgs): { + from: FlatEntityMaps; + to: FlatEntityMaps; +} => { + const fromFlatEntityMaps = + flatEntityToDelete.length > 0 + ? getSubFlatEntityMapsOrThrow({ + flatEntityIds: [...flatEntityToDelete, ...flatEntityToUpdate].map( + ({ id }) => id, + ), + flatEntityMaps, + }) + : flatEntityMaps; + + const toFlatEntityMapsWithDeleted = flatEntityToDelete.reduce( + (flatEntityMaps, flatEntity) => + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatEntity.id, + flatEntityMaps, + }), + fromFlatEntityMaps, + ); + + const toFlatEntityMapsWithUpdated = flatEntityToUpdate.reduce( + (flatEntityMaps, flatEntity) => + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity, + flatEntityMaps, + }), + toFlatEntityMapsWithDeleted, + ); + + const toFlatEntityMaps = flatEntityToCreate.reduce( + (flatEntityMaps, flatEntity) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity, + flatEntityMaps, + }), + toFlatEntityMapsWithUpdated, + ); + + return { + from: fromFlatEntityMaps, + to: toFlatEntityMaps, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts index 56f6df7d6a..c08f5031ef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock.ts @@ -19,6 +19,8 @@ export const getFlatFieldMetadataMock = ( const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date; return { + viewFilterIds: [], + viewGroupIds: [], viewFieldIds: [], createdAt, updatedAt: createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts index 927372cbf1..2fb9e4a254 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__mocks__/get-morph-or-relation-target-flat-field-metadata-mock.ts @@ -32,6 +32,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({ const createdAt = '2024-01-01T00:00:00.000Z' as unknown as Date; return { + viewFilterIds: [], + viewGroupIds: [], viewFieldIds: [], createdAt, updatedAt: createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap index fd365731fa..656a3bdd99 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/__tests__/__snapshots__/morph-relation-from-create-field-input-to-flat-field-metadatas-to-create.spec.ts.snap @@ -132,6 +132,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "universalIdentifier": Any, "updatedAt": Any, "viewFieldIds": [], + "viewFilterIds": [], + "viewGroupIds": [], "workspaceId": Any, }, { @@ -165,6 +167,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "universalIdentifier": Any, "updatedAt": Any, "viewFieldIds": [], + "viewFilterIds": [], + "viewGroupIds": [], "workspaceId": Any, }, { @@ -196,6 +200,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "universalIdentifier": Any, "updatedAt": Any, "viewFieldIds": [], + "viewFilterIds": [], + "viewGroupIds": [], "workspaceId": Any, }, { @@ -229,6 +235,8 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su "universalIdentifier": Any, "updatedAt": Any, "viewFieldIds": [], + "viewFilterIds": [], + "viewGroupIds": [], "workspaceId": Any, }, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts index 027f93823b..fca743b8e0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts @@ -42,9 +42,15 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapC viewFields: { id: true, }, + viewFilters: { + id: true, + }, + viewGroups: { + id: true, + }, }, withDeleted: true, - relations: ['viewFields'], + relations: ['viewFields', 'viewFilters', 'viewGroups'], }); return fieldMetadatas.reduce( diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts index 862f4c6d66..78380d955c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type.ts @@ -2,20 +2,24 @@ import { type FieldMetadataType } from 'twenty-shared/types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -export const fieldMetadataRelationProperties = [ +export const FIELD_METADATA_RELATION_PROPERTIES = [ 'relationTargetFieldMetadata', 'relationTargetObjectMetadata', 'fieldPermissions', 'indexFieldMetadatas', 'object', 'viewFields', + 'viewFilters', + 'viewGroups', ] as const satisfies (keyof FieldMetadataEntity)[]; export type FieldMetadataEntityRelationProperties = - (typeof fieldMetadataRelationProperties)[number]; + (typeof FIELD_METADATA_RELATION_PROPERTIES)[number]; export type FlatFieldMetadata = Omit, FieldMetadataEntityRelationProperties> & { universalIdentifier: string; viewFieldIds: string[]; + viewFilterIds: string[]; + viewGroupIds: string[]; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/compare-two-flat-field-metadata-enum-options.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/compare-two-flat-field-metadata-enum-options.spec.ts new file mode 100644 index 0000000000..ce752ba8ff --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/compare-two-flat-field-metadata-enum-options.spec.ts @@ -0,0 +1,349 @@ +import { + type EachTestingContext, + eachTestingContextFilter, +} from 'twenty-shared/testing'; + +import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { + type CompareToFlatFieldMetadataEnumOptionsArgs, + compareTwoFlatFieldMetadataEnumOptions, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util'; + +type GetOptionsDifferencesTestContext = EachTestingContext< + CompareToFlatFieldMetadataEnumOptionsArgs & { + expected: { + created: FieldMetadataDefaultOption[]; + updated: { + from: FieldMetadataDefaultOption; + to: FieldMetadataDefaultOption; + }[]; + deleted: FieldMetadataDefaultOption[]; + }; + } +>; + +describe('compareTwoFlatFieldMetadataEnumOptions', () => { + const testCases: GetOptionsDifferencesTestContext[] = [ + { + title: 'should identify created options', + context: { + compareLabel: false, + fromOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + ], + toOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + expected: { + created: [ + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + updated: [], + deleted: [], + }, + }, + }, + { + title: 'should identify updated options', + context: { + compareLabel: false, + fromOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + ], + toOptions: [ + { + id: '1', + label: 'Option 1', + value: 'updated-value1', + position: 0, + }, + ], + expected: { + created: [], + updated: [ + { + from: { + id: '1', + label: 'Option 1', + value: 'value1', + position: 0, + }, + to: { + id: '1', + label: 'Option 1', + value: 'updated-value1', + position: 0, + }, + }, + ], + deleted: [], + }, + }, + }, + { + title: 'should identify deleted options', + context: { + compareLabel: false, + fromOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + toOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + ], + expected: { + created: [], + updated: [], + deleted: [ + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + }, + }, + }, + { + title: 'should identify all types of changes', + context: { + compareLabel: false, + fromOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + { id: '3', label: 'Option 3', value: 'value3', position: 2 }, + ], + toOptions: [ + { + id: '1', + label: 'Option 1', + value: 'updated-value1', + position: 0, + }, + { id: '3', label: 'Option 3', value: 'value3', position: 1 }, + { id: '4', label: 'Option 4', value: 'value4', position: 2 }, + ], + expected: { + created: [ + { id: '4', label: 'Option 4', value: 'value4', position: 2 }, + ], + updated: [ + { + from: { + id: '1', + label: 'Option 1', + value: 'value1', + position: 0, + }, + to: { + id: '1', + label: 'Option 1', + value: 'updated-value1', + position: 0, + }, + }, + ], + deleted: [ + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + }, + }, + }, + { + title: 'should handle empty arrays', + context: { + compareLabel: false, + fromOptions: [], + toOptions: [], + expected: { + created: [], + updated: [], + deleted: [], + }, + }, + }, + { + title: 'should handle all new options', + context: { + compareLabel: false, + fromOptions: [], + toOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + expected: { + created: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + updated: [], + deleted: [], + }, + }, + }, + { + title: 'should handle all deleted options', + context: { + compareLabel: false, + fromOptions: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + toOptions: [], + expected: { + created: [], + updated: [], + deleted: [ + { id: '1', label: 'Option 1', value: 'value1', position: 0 }, + { id: '2', label: 'Option 2', value: 'value2', position: 1 }, + ], + }, + }, + }, + { + title: + 'should not consider changes to label as updates when value remains the same', + context: { + compareLabel: false, + fromOptions: [ + { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0', + value: 'option0', + position: 1, + }, + { + id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', + label: 'Option 1', + value: 'option1', + position: 2, + }, + { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2', + value: 'option2', + position: 3, + }, + ], + toOptions: [ + { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0_UPDATED', // Label changed but value remains the same + value: 'option0', + position: 1, + }, + { + id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', + label: 'Option 1', // No change + value: 'option1', + position: 2, + }, + { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2_UPDATED', // Label changed but value remains the same + value: 'option2', + position: 3, + }, + ], + expected: { + created: [], + updated: [], // No updates because only labels changed, not values + deleted: [], + }, + }, + }, + { + title: + 'should consider changes to label as updates when value remains the same if compareLabel is true', + context: { + fromOptions: [ + { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0', + value: 'option0', + position: 1, + }, + { + id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', + label: 'Option 1', + value: 'option1', + position: 2, + }, + { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2', + value: 'option2', + position: 3, + }, + ], + toOptions: [ + { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0_UPDATED', // Label changed but value remains the same + value: 'option0', + position: 1, + }, + { + id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', + label: 'Option 1', // No change + value: 'option1', + position: 2, + }, + { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2_UPDATED', // Label changed but value remains the same + value: 'option2', + position: 3, + }, + ], + expected: { + created: [], + updated: [ + { + to: { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0_UPDATED', + position: 1, + value: 'option0', + }, + from: { + id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', + label: 'Option 0', + position: 1, + value: 'option0', + }, + }, + { + to: { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2_UPDATED', + position: 3, + value: 'option2', + }, + from: { + id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', + label: 'Option 2', + position: 3, + value: 'option2', + }, + }, + ], + deleted: [], + }, + compareLabel: true, + }, + }, + ]; + + test.each(eachTestingContextFilter(testCases))( + '$title', + ({ context: { fromOptions, toOptions, expected, compareLabel } }) => { + const result = compareTwoFlatFieldMetadataEnumOptions({ + compareLabel: compareLabel ?? false, + fromOptions: fromOptions, + toOptions: toOptions, + }); + + expect(result.created).toEqual(expected.created); + expect(result.updated).toEqual(expected.updated); + expect(result.deleted).toEqual(expected.deleted); + }, + ); +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util.ts new file mode 100644 index 0000000000..8a94ce91ff --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util.ts @@ -0,0 +1,61 @@ +import { type EnumFieldMetadataType, type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; + +type FieldMetadataEnumOptions = + FlatFieldMetadata['options']; + +type Differences = { + created: T[]; + updated: { from: T; to: T }[]; + deleted: T[]; +}; +export type CompareToFlatFieldMetadataEnumOptionsArgs = FromTo< + FieldMetadataEnumOptions, + 'options' +> & { + compareLabel: boolean; +}; +export const compareTwoFlatFieldMetadataEnumOptions = ({ + compareLabel = false, + fromOptions, + toOptions, +}: CompareToFlatFieldMetadataEnumOptionsArgs): Differences< + FieldMetadataEnumOptions[number] +> => { + const differences: Differences = { + created: [], + updated: [], + deleted: [], + }; + + const fromOptionsMap = new Map(fromOptions.map((opt) => [opt.id, opt])); + + for (const newOption of toOptions) { + const oldOption = fromOptionsMap.get(newOption.id); + + if (!isDefined(oldOption)) { + differences.created.push(newOption); + continue; + } + + if ( + oldOption.value !== newOption.value || + (compareLabel && oldOption.label !== newOption.label) + ) { + differences.updated.push({ from: oldOption, to: newOption }); + continue; + } + } + + const toOptionsMap = new Map(toOptions.map((opt) => [opt.id, opt])); + + for (const oldOption of fromOptions) { + if (!toOptionsMap.has(oldOption.id)) { + differences.deleted.push(oldOption); + } + } + + return differences; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts index 99f356f9df..0a66e6bd79 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util.ts @@ -4,7 +4,7 @@ import { removePropertiesFromRecord } from 'twenty-shared/utils'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { type FlatFieldMetadata, - fieldMetadataRelationProperties, + FIELD_METADATA_RELATION_PROPERTIES, } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const fromFieldMetadataEntityToFlatFieldMetadata = < @@ -15,14 +15,14 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = < ): FlatFieldMetadata => { const fieldMetadataWithoutRelations = removePropertiesFromRecord( fieldMetadataEntity, - fieldMetadataRelationProperties, + FIELD_METADATA_RELATION_PROPERTIES, ); return { ...fieldMetadataWithoutRelations, - viewFieldIds: fieldMetadataEntity.viewFields.map( - (viewField) => viewField.id, - ), + viewGroupIds: fieldMetadataEntity.viewGroups.map(({ id }) => id), + viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id), + viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id), universalIdentifier: fieldMetadataWithoutRelations.standardId ?? fieldMetadataWithoutRelations.id, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index 8d839d0859..fdf2a833cb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -16,110 +16,36 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; -import { type FlatFieldMetadataEditableProperties } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-editable-properties.constant'; 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 { handleIndexChangesDuringFieldUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util'; -import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; +import { + type FlatFieldMetadataUpdateSideEffects, + handleFlatFieldMetadataUpdateSideEffect, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util'; import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; - -type UpdatedFlatFieldMetadataAndIndexToUpdate = { - flatFieldMetadata: FlatFieldMetadata; -}; - -type SanitizedUpdateFieldInput = ReturnType< - typeof extractAndSanitizeObjectStringFields< - UpdateFieldInput, - FlatFieldMetadataEditableProperties[] - > ->; - -type ApplyUpdatesToFlatFieldMetadataArgs = { - updatedEditableFieldProperties: SanitizedUpdateFieldInput; - fromFlatFieldMetadata: FlatFieldMetadata; -} & Pick< - AllFlatEntityMaps, - 'flatIndexMaps' | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' ->; - -const applyUpdatesToFlatFieldMetadata = ({ - updatedEditableFieldProperties, - fromFlatFieldMetadata, - flatObjectMetadataMaps, - flatIndexMaps, - flatFieldMetadataMaps, -}: ApplyUpdatesToFlatFieldMetadataArgs): { - flatFieldMetadata: FlatFieldMetadata; - flatIndexMetadataToUpdate: FlatIndexMetadata[]; - flatIndexMetadatasToDelete: FlatIndexMetadata[]; - flatIndexMetadatasToCreate: FlatIndexMetadata[]; -} => { - const { flatFieldMetadata: updatedFlatFieldMetadata } = - FLAT_FIELD_METADATA_EDITABLE_PROPERTIES.reduce( - ({ flatFieldMetadata }, property) => { - const updatedPropertyValue = updatedEditableFieldProperties[property]; - const isPropertyUpdated = - updatedPropertyValue !== undefined && - flatFieldMetadata[property] !== updatedPropertyValue; - - if (!isPropertyUpdated) { - return { flatFieldMetadata }; - } - - const updatedFlatFieldMetadata = { - ...flatFieldMetadata, - [property]: updatedPropertyValue, - }; - - if (property === 'options') { - updatedFlatFieldMetadata.options = - updatedEditableFieldProperties[property]?.map((option) => ({ - id: v4(), - ...option, - })) ?? []; - } - - return { - flatFieldMetadata: updatedFlatFieldMetadata, - }; - }, - { - flatFieldMetadata: structuredClone(fromFlatFieldMetadata), - }, - ); - - const indexChanges = handleIndexChangesDuringFieldUpdate({ - originalFlatFieldMetadata: fromFlatFieldMetadata, - updatedFlatFieldMetadata, - flatIndexMaps, - flatObjectMetadataMaps, - flatFieldMetadataMaps, - }); - - return { - flatFieldMetadata: updatedFlatFieldMetadata, - ...indexChanges, - }; -}; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; type FromUpdateFieldInputToFlatFieldMetadataArgs = { updateFieldInput: UpdateFieldInput; } & Pick< AllFlatEntityMaps, - 'flatObjectMetadataMaps' | 'flatIndexMaps' | 'flatFieldMetadataMaps' + | 'flatObjectMetadataMaps' + | 'flatIndexMaps' + | 'flatFieldMetadataMaps' + | 'flatViewFilterMaps' + | 'flatViewGroupMaps' >; type FlatFieldMetadataAndIndexToUpdate = { flatFieldMetadatasToUpdate: FlatFieldMetadata[]; - flatIndexMetadatasToUpdate: FlatIndexMetadata[]; - flatIndexMetadatasToDelete: FlatIndexMetadata[]; - flatIndexMetadatasToCreate: FlatIndexMetadata[]; -}; +} & FlatFieldMetadataUpdateSideEffects; export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatIndexMaps, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatFieldMetadataMaps, updateFieldInput: rawUpdateFieldInput, + flatViewFilterMaps, + flatViewGroupMaps, }: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [ @@ -188,10 +114,15 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ return { status: 'success', result: { + flatViewGroupsToCreate: [], + flatViewGroupsToDelete: [], + flatViewGroupsToUpdate: [], flatFieldMetadatasToUpdate: [updatedStandardFlatFieldMetadata], flatIndexMetadatasToUpdate: [], flatIndexMetadatasToDelete: [], flatIndexMetadatasToCreate: [], + flatViewFiltersToDelete: [], + flatViewFiltersToUpdate: [], }, }; } @@ -220,47 +151,95 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ ...relatedFlatFieldMetadatasToUpdate, ]; + const initialAccumulator: FlatFieldMetadataAndIndexToUpdate = { + flatFieldMetadatasToUpdate: [], + flatIndexMetadatasToUpdate: [], + flatViewFiltersToDelete: [], + flatViewFiltersToUpdate: [], + flatViewGroupsToCreate: [], + flatViewGroupsToDelete: [], + flatIndexMetadatasToCreate: [], + flatIndexMetadatasToDelete: [], + flatViewGroupsToUpdate: [], + }; + + updatedEditableFieldProperties.options = !isDefined( + updatedEditableFieldProperties.options, + ) + ? updatedEditableFieldProperties.options + : updatedEditableFieldProperties.options.map((option) => ({ + id: v4(), + ...option, + })); + const optimisticiallyUpdatedFlatFieldMetadatas = flatFieldMetadatasToUpdate.reduce( - (acc, fromFlatFieldMetadata) => { + (accumulator, fromFlatFieldMetadata) => { + const toFlatFieldMetadata = mergeUpdateInExistingRecord({ + existing: fromFlatFieldMetadata, + properties: FLAT_FIELD_METADATA_EDITABLE_PROPERTIES, + update: updatedEditableFieldProperties, + }); + const { - flatFieldMetadata, - flatIndexMetadataToUpdate, - flatIndexMetadatasToDelete, + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatViewGroupsToUpdate, + flatIndexMetadatasToUpdate, + flatViewFiltersToDelete, + flatViewFiltersToUpdate, flatIndexMetadatasToCreate, - } = applyUpdatesToFlatFieldMetadata({ + flatIndexMetadatasToDelete, + } = handleFlatFieldMetadataUpdateSideEffect({ + flatViewFilterMaps, + flatViewGroupMaps, flatObjectMetadataMaps: existingFlatObjectMetadataMaps, fromFlatFieldMetadata, flatFieldMetadataMaps, flatIndexMaps, - updatedEditableFieldProperties, + toFlatFieldMetadata, }); return { flatFieldMetadatasToUpdate: [ - ...acc.flatFieldMetadatasToUpdate, - flatFieldMetadata, + ...accumulator.flatFieldMetadatasToUpdate, + toFlatFieldMetadata, ], flatIndexMetadatasToUpdate: [ - ...acc.flatIndexMetadatasToUpdate, - ...flatIndexMetadataToUpdate, + ...accumulator.flatIndexMetadatasToUpdate, + ...flatIndexMetadatasToUpdate, + ], + flatViewFiltersToDelete: [ + ...accumulator.flatViewFiltersToDelete, + ...flatViewFiltersToDelete, + ], + flatViewFiltersToUpdate: [ + ...accumulator.flatViewFiltersToUpdate, + ...flatViewFiltersToUpdate, + ], + flatViewGroupsToCreate: [ + ...accumulator.flatViewGroupsToCreate, + ...flatViewGroupsToCreate, + ], + flatViewGroupsToDelete: [ + ...accumulator.flatViewGroupsToDelete, + ...flatViewGroupsToDelete, + ], + flatViewGroupsToUpdate: [ + ...accumulator.flatViewGroupsToUpdate, + ...flatViewGroupsToUpdate, ], flatIndexMetadatasToDelete: [ - ...acc.flatIndexMetadatasToDelete, + ...accumulator.flatIndexMetadatasToDelete, ...flatIndexMetadatasToDelete, ], flatIndexMetadatasToCreate: [ - ...acc.flatIndexMetadatasToCreate, + ...accumulator.flatIndexMetadatasToCreate, ...flatIndexMetadatasToCreate, ], }; }, - { - flatFieldMetadatasToUpdate: [], - flatIndexMetadatasToUpdate: [], - flatIndexMetadatasToDelete: [], - flatIndexMetadatasToCreate: [], - }, + initialAccumulator, ); return { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts index dffa44ba9d..cf6d3edf66 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util.ts @@ -37,8 +37,8 @@ export const getDefaultFlatFieldMetadata = ({ ), isSystem: false, isUnique: createFieldInput.isUnique ?? null, - label: createFieldInput.label ?? null, - name: createFieldInput.name ?? null, + label: createFieldInput.label, + name: createFieldInput.name, objectMetadataId: createFieldInput.objectMetadataId, relationTargetFieldMetadataId: null, relationTargetObjectMetadataId: null, @@ -54,5 +54,7 @@ export const getDefaultFlatFieldMetadata = ({ updatedAt: createdAt, isUIReadOnly: createFieldInput.isUIReadOnly ?? false, morphId: null, + viewFilterIds: [], + viewGroupIds: [], } as const satisfies FlatFieldMetadata; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts new file mode 100644 index 0000000000..f78412aa9d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util.ts @@ -0,0 +1,103 @@ +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, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util'; +import { recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util'; +import { + type FlatViewGroupsToDeleteUpdateAndCreate, + recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util'; +import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; + +type HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs = FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' +> & + Pick; + +type EnumFieldMetadataSideEffectResult = FlatViewGroupsToDeleteUpdateAndCreate & + FlatViewFiltersToDeleteAndUpdate; + +const EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT: EnumFieldMetadataSideEffectResult = + { + flatViewFiltersToDelete: [], + flatViewFiltersToUpdate: [], + flatViewGroupsToCreate: [], + flatViewGroupsToDelete: [], + flatViewGroupsToUpdate: [], + }; + +export const handleEnumFlatFieldMetadataUpdateSideEffects = ({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatViewFilterMaps, + flatViewGroupMaps, +}: HandleEnumFlatFieldMetadataOptionsUpdateSideEffectsArgs): EnumFieldMetadataSideEffectResult => { + const sideEffectResult = structuredClone( + EMPTY_ENUM_FIELD_METADATA_SIDE_EFFECT_RESULT, + ); + + if ( + !isEnumFlatFieldMetadata(toFlatFieldMetadata) || + !isEnumFlatFieldMetadata(fromFlatFieldMetadata) + ) { + return sideEffectResult; + } + + if ( + JSON.stringify(fromFlatFieldMetadata.options) !== + JSON.stringify(toFlatFieldMetadata.options) + ) { + const optionsPropertyUpdate: PropertyUpdate< + FlatFieldMetadata, + 'options' + > = { + from: fromFlatFieldMetadata.options, + property: 'options', + to: toFlatFieldMetadata.options, + }; + + const { flatViewFiltersToDelete, flatViewFiltersToUpdate } = + recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate({ + flatViewFilterMaps, + fromFlatFieldMetadata, + update: optionsPropertyUpdate, + }); + + sideEffectResult.flatViewFiltersToDelete.push(...flatViewFiltersToDelete); + sideEffectResult.flatViewFiltersToUpdate.push(...flatViewFiltersToUpdate); + + const { + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatViewGroupsToUpdate, + } = recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate({ + flatViewGroupMaps, + fromFlatFieldMetadata, + update: optionsPropertyUpdate, + }); + + sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate); + sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete); + sideEffectResult.flatViewGroupsToUpdate.push(...flatViewGroupsToUpdate); + } + + if (fromFlatFieldMetadata.isNullable !== toFlatFieldMetadata.isNullable) { + const { flatViewGroupsToCreate, flatViewGroupsToDelete } = + recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate({ + flatViewGroupMaps, + fromFlatFieldMetadata, + toFlatFieldMetadata, + }); + + sideEffectResult.flatViewGroupsToCreate.push(...flatViewGroupsToCreate); + sideEffectResult.flatViewGroupsToDelete.push(...flatViewGroupsToDelete); + } + + return sideEffectResult; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts new file mode 100644 index 0000000000..15e4b0b74f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts @@ -0,0 +1,75 @@ +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 FieldMetadataUpdateIndexSideEffect, + handleIndexChangesDuringFieldUpdate, +} from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.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; + +type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' +> & + Pick< + AllFlatEntityMaps, + | 'flatIndexMaps' + | 'flatObjectMetadataMaps' + | 'flatFieldMetadataMaps' + | 'flatViewFilterMaps' + | 'flatViewGroupMaps' + >; + +export const handleFlatFieldMetadataUpdateSideEffect = ({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatObjectMetadataMaps, + flatIndexMaps, + flatFieldMetadataMaps, + flatViewFilterMaps, + flatViewGroupMaps, +}: HandleFlatFieldMetadataUpdateSideEffectArgs): FlatFieldMetadataUpdateSideEffects => { + const { + flatViewFiltersToDelete, + flatViewFiltersToUpdate, + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatViewGroupsToUpdate, + } = handleEnumFlatFieldMetadataUpdateSideEffects({ + flatViewFilterMaps, + flatViewGroupMaps, + fromFlatFieldMetadata, + toFlatFieldMetadata, + }); + + const { + flatIndexMetadatasToUpdate, + flatIndexMetadatasToCreate, + flatIndexMetadatasToDelete, + } = handleIndexChangesDuringFieldUpdate({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatIndexMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }); + + return { + flatIndexMetadatasToUpdate, + flatViewFiltersToDelete, + flatViewFiltersToUpdate, + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatIndexMetadatasToDelete, + flatIndexMetadatasToCreate, + flatViewGroupsToUpdate, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts index ffaabc99f3..4368830fbc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util.ts @@ -1,3 +1,5 @@ +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 FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; @@ -8,61 +10,64 @@ import { recomputeIndexOnFlatFieldMetadataNameUpdate } from 'src/engine/metadata import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -type IndexChanges = { - flatIndexMetadataToUpdate: FlatIndexMetadata[]; +export type FieldMetadataUpdateIndexSideEffect = { + flatIndexMetadatasToUpdate: FlatIndexMetadata[]; flatIndexMetadatasToDelete: FlatIndexMetadata[]; flatIndexMetadatasToCreate: FlatIndexMetadata[]; }; -const NO_INDEX_CHANGES: IndexChanges = { - flatIndexMetadataToUpdate: [], - flatIndexMetadatasToDelete: [], - flatIndexMetadatasToCreate: [], -}; +type FromToFlatFieldMetadataAndFlatEntityMaps = FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' +> & + Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps' | 'flatIndexMaps' + >; +const FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT: FieldMetadataUpdateIndexSideEffect = + { + flatIndexMetadatasToUpdate: [], + flatIndexMetadatasToDelete: [], + flatIndexMetadatasToCreate: [], + }; export const handleIndexChangesDuringFieldUpdate = ({ - originalFlatFieldMetadata, - updatedFlatFieldMetadata, + fromFlatFieldMetadata, + toFlatFieldMetadata, flatIndexMaps, flatObjectMetadataMaps, flatFieldMetadataMaps, -}: { - originalFlatFieldMetadata: FlatFieldMetadata; - updatedFlatFieldMetadata: FlatFieldMetadata; - flatIndexMaps: FlatEntityMaps; - flatObjectMetadataMaps: FlatEntityMaps; - flatFieldMetadataMaps: FlatEntityMaps; -}): IndexChanges => { +}: FromToFlatFieldMetadataAndFlatEntityMaps): FieldMetadataUpdateIndexSideEffect => { if ( !hasIndexRelevantChanges({ - originalFlatFieldMetadata, - updatedFlatFieldMetadata, + fromFlatFieldMetadata, + toFlatFieldMetadata, }) ) { - return NO_INDEX_CHANGES; + return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT; } const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: originalFlatFieldMetadata.objectMetadataId, + flatEntityId: fromFlatFieldMetadata.objectMetadataId, }); const relatedIndexes = findRelatedIndexes({ - flatFieldMetadata: originalFlatFieldMetadata, + flatFieldMetadata: fromFlatFieldMetadata, flatObjectMetadata, flatIndexMaps, }); if (relatedIndexes.length === 0) { return handleNoExistingIndexes({ - updatedFlatFieldMetadata, + toFlatFieldMetadata, flatObjectMetadata, }); } return handleExistingIndexes({ - updatedFlatFieldMetadata, - originalFlatFieldMetadata, + toFlatFieldMetadata, + fromFlatFieldMetadata, relatedIndexes, flatObjectMetadata, flatFieldMetadataMaps, @@ -70,14 +75,11 @@ export const handleIndexChangesDuringFieldUpdate = ({ }; const hasIndexRelevantChanges = ({ - originalFlatFieldMetadata, - updatedFlatFieldMetadata, -}: { - originalFlatFieldMetadata: FlatFieldMetadata; - updatedFlatFieldMetadata: FlatFieldMetadata; -}): boolean => - originalFlatFieldMetadata.name !== updatedFlatFieldMetadata.name || - originalFlatFieldMetadata.isUnique !== updatedFlatFieldMetadata.isUnique; + fromFlatFieldMetadata, + toFlatFieldMetadata, +}: FromTo): boolean => + fromFlatFieldMetadata.name !== toFlatFieldMetadata.name || + fromFlatFieldMetadata.isUnique !== toFlatFieldMetadata.isUnique; const findRelatedIndexes = ({ flatFieldMetadata, @@ -101,45 +103,46 @@ const findRelatedIndexes = ({ }; const handleNoExistingIndexes = ({ - updatedFlatFieldMetadata, + toFlatFieldMetadata, flatObjectMetadata, }: { - updatedFlatFieldMetadata: FlatFieldMetadata; + toFlatFieldMetadata: FlatFieldMetadata; flatObjectMetadata: FlatObjectMetadata; -}): IndexChanges => { - if (!updatedFlatFieldMetadata.isUnique) { - return NO_INDEX_CHANGES; +}): FieldMetadataUpdateIndexSideEffect => { + if (!toFlatFieldMetadata.isUnique) { + return FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT; } const newIndex = generateIndexForFlatFieldMetadata({ - flatFieldMetadata: updatedFlatFieldMetadata, + flatFieldMetadata: toFlatFieldMetadata, flatObjectMetadata, workspaceId: flatObjectMetadata.workspaceId, }); return { - ...NO_INDEX_CHANGES, + ...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT, flatIndexMetadatasToCreate: [newIndex], }; }; const handleExistingIndexes = ({ - updatedFlatFieldMetadata, - originalFlatFieldMetadata, + fromFlatFieldMetadata, + toFlatFieldMetadata, relatedIndexes, flatObjectMetadata, flatFieldMetadataMaps, }: { - updatedFlatFieldMetadata: FlatFieldMetadata; - originalFlatFieldMetadata: FlatFieldMetadata; relatedIndexes: FlatIndexMetadata[]; flatObjectMetadata: FlatObjectMetadata; flatFieldMetadataMaps: AllFlatEntityMaps['flatFieldMetadataMaps']; -}): IndexChanges => { - if (updatedFlatFieldMetadata.isUnique === false) { +} & FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' +>): FieldMetadataUpdateIndexSideEffect => { + if (toFlatFieldMetadata.isUnique === false) { const expectedUniqueIndex = generateIndexForFlatFieldMetadata({ flatFieldMetadata: { - ...originalFlatFieldMetadata, + ...fromFlatFieldMetadata, isUnique: true, }, flatObjectMetadata, @@ -151,7 +154,7 @@ const handleExistingIndexes = ({ ); return { - ...NO_INDEX_CHANGES, + ...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT, flatIndexMetadatasToDelete: uniqueIndexToDelete ? [uniqueIndexToDelete] : [], @@ -161,16 +164,16 @@ const handleExistingIndexes = ({ const updatedIndexes = recomputeIndexOnFlatFieldMetadataNameUpdate({ flatFieldMetadataMaps, flatObjectMetadata, - fromFlatFieldMetadata: originalFlatFieldMetadata, + fromFlatFieldMetadata, toFlatFieldMetadata: { - name: updatedFlatFieldMetadata.name, - isUnique: updatedFlatFieldMetadata.isUnique, + name: toFlatFieldMetadata.name, + isUnique: toFlatFieldMetadata.isUnique, }, relatedFlatIndexMetadata: relatedIndexes, }); return { - ...NO_INDEX_CHANGES, - flatIndexMetadataToUpdate: updatedIndexes, + ...FIELD_METADATA_UPDATE_INDEX_SIDE_EFFECT, + flatIndexMetadatasToUpdate: updatedIndexes, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util.ts new file mode 100644 index 0000000000..888a947b45 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util.ts @@ -0,0 +1,119 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { type EnumFieldMetadataType } from 'twenty-shared/types'; +import { isDefined, parseJson } from 'twenty-shared/utils'; + +import { + FieldMetadataException, + FieldMetadataExceptionCode, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +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 { compareTwoFlatFieldMetadataEnumOptions } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util'; +import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type'; +import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; + +type RecomputeViewFiltersOnFlatFieldMetadataOptionsUpdateArgs = { + fromFlatFieldMetadata: FlatFieldMetadata; + update: PropertyUpdate, 'options'>; +} & Pick; + +export type FlatViewFiltersToDeleteAndUpdate = { + flatViewFiltersToDelete: FlatViewFilter[]; + flatViewFiltersToUpdate: FlatViewFilter[]; +}; +export const recomputeViewFiltersOnFlatFieldMetadataOptionsUpdate = ({ + flatViewFilterMaps, + fromFlatFieldMetadata, + update, +}: RecomputeViewFiltersOnFlatFieldMetadataOptionsUpdateArgs): FlatViewFiltersToDeleteAndUpdate => { + const flatViewFiltersToCreateAndUpdate: FlatViewFiltersToDeleteAndUpdate = { + flatViewFiltersToDelete: [], + flatViewFiltersToUpdate: [], + }; + + const { + deleted: deletedFieldMetadataOptions, + updated: updatedFieldMetadataOptions, + } = compareTwoFlatFieldMetadataEnumOptions({ + compareLabel: false, + fromOptions: fromFlatFieldMetadata.options, + toOptions: update.to, + }); + + if ( + deletedFieldMetadataOptions.length === 0 && + updatedFieldMetadataOptions.length === 0 + ) { + return flatViewFiltersToCreateAndUpdate; + } + + const flatViewFilters = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.viewFilterIds, + flatEntityMaps: flatViewFilterMaps, + }); + + for (const viewFilter of flatViewFilters) { + const rawViewFilterValue = viewFilter.value; + + if (!isDefined(rawViewFilterValue)) { + continue; + } + + // TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data) + const viewFilterValue = isNonEmptyString(rawViewFilterValue) + ? parseJson(rawViewFilterValue) + : rawViewFilterValue; + + if (!isDefined(viewFilterValue) || !Array.isArray(viewFilterValue)) { + throw new FieldMetadataException( + `Unexpected invalid view filter value for filter ${viewFilter.id}`, + FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + + const viewFilterOptions = viewFilterValue + .flatMap((value) => { + if (!isDefined(fromFlatFieldMetadata.options)) { + return undefined; + } + + return fromFlatFieldMetadata.options.find( + (option) => option.value === value, + ); + }) + .filter(isDefined); + + const afterDeleteViewFilterOptions = viewFilterOptions.filter( + (viewFilterOption) => + !deletedFieldMetadataOptions.some( + (option) => option.value === viewFilterOption.value, + ), + ); + + if (afterDeleteViewFilterOptions.length === 0) { + flatViewFiltersToCreateAndUpdate.flatViewFiltersToDelete.push(viewFilter); + continue; + } + + const afterUpdateAndDeleteViewFilterOptions = + afterDeleteViewFilterOptions.map((viewFilterOption) => { + const updatedOption = updatedFieldMetadataOptions.find( + ({ from }) => viewFilterOption.value === from.value, + ); + + return isDefined(updatedOption) ? updatedOption.to : viewFilterOption; + }); + + const optionsValues = afterUpdateAndDeleteViewFilterOptions.map( + (option) => option.value, + ); + + flatViewFiltersToCreateAndUpdate.flatViewFiltersToUpdate.push({ + ...viewFilter, + value: optionsValues, + }); + } + + return flatViewFiltersToCreateAndUpdate; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts new file mode 100644 index 0000000000..0164bd3121 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-enum-flat-field-metadata-is-nullable-update.util.ts @@ -0,0 +1,81 @@ +import { type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; + +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 FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; +import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util'; + +type RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs = FromTo< + FlatFieldMetadata, + 'flatFieldMetadata' +> & + Pick; + +type EnumFieldMetadataIsNullableUpdateSideEffect = { + flatViewGroupsToDelete: FlatViewGroup[]; + flatViewGroupsToCreate: FlatViewGroup[]; +}; +const EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT: EnumFieldMetadataIsNullableUpdateSideEffect = + { + flatViewGroupsToCreate: [], + flatViewGroupsToDelete: [], + }; + +export const recomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdate = ({ + flatViewGroupMaps: allFlatViewGroups, + fromFlatFieldMetadata, + toFlatFieldMetadata, +}: RecomputeViewGroupsOnEnumFlatFieldMetadataIsNullableUpdateArgs): { + flatViewGroupsToDelete: FlatViewGroup[]; + flatViewGroupsToCreate: FlatViewGroup[]; +} => { + const sideEffectResult = structuredClone( + EMPTY_ENUM_FIELD_METADATA_IS_NULLABLE_UPDATE_SIDE_EFFECT_RESULT, + ); + const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.viewGroupIds, + flatEntityMaps: allFlatViewGroups, + }); + const { flatViewGroupRecordByViewId, highestViewGroupPositionByViewId } = + reduceFlatViewGroupsByViewId({ + flatViewGroups, + }); + + for (const viewId in flatViewGroupRecordByViewId) { + const flatViewGroups = Object.values(flatViewGroupRecordByViewId[viewId]); + + const emptyValueFlatViewGroup = flatViewGroups.find( + (flatViewGroup) => flatViewGroup.fieldValue === '', + ); + + if ( + toFlatFieldMetadata.isNullable === true && + !isDefined(emptyValueFlatViewGroup) + ) { + const highestViewGroupPosition = highestViewGroupPositionByViewId[viewId]; + const viewGroupId = v4(); + const createdAt = new Date(); + + sideEffectResult.flatViewGroupsToCreate.push({ + fieldMetadataId: toFlatFieldMetadata.id, + id: viewGroupId, + universalIdentifier: viewGroupId, + fieldValue: '', + position: highestViewGroupPosition + 1, + isVisible: true, + workspaceId: toFlatFieldMetadata.workspaceId, + createdAt, + updatedAt: createdAt, + deletedAt: null, + viewId, + }); + } else if (isDefined(emptyValueFlatViewGroup)) { + sideEffectResult.flatViewGroupsToDelete.push(emptyValueFlatViewGroup); + } + } + + return sideEffectResult; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts new file mode 100644 index 0000000000..057624d98a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util.ts @@ -0,0 +1,121 @@ +import { isDefined } from 'class-validator'; +import { type EnumFieldMetadataType } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { + FlatEntityMapsException, + FlatEntityMapsExceptionCode, +} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; +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 { compareTwoFlatFieldMetadataEnumOptions } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata-enum-options.util'; +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; +import { reduceFlatViewGroupsByViewId } from 'src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util'; +import { type PropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/types/property-update.type'; + +type RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs = { + fromFlatFieldMetadata: FlatFieldMetadata; + update: PropertyUpdate, 'options'>; +} & Pick; + +export type FlatViewGroupsToDeleteUpdateAndCreate = { + flatViewGroupsToDelete: FlatViewGroup[]; + flatViewGroupsToUpdate: FlatViewGroup[]; + flatViewGroupsToCreate: FlatViewGroup[]; +}; +export const recomputeViewGroupsOnFlatFieldMetadataOptionsUpdate = ({ + flatViewGroupMaps, + fromFlatFieldMetadata, + update, +}: RecomputeViewGroupsOnFlatFieldMetadataOptionsUpdateArgs): FlatViewGroupsToDeleteUpdateAndCreate => { + const { + deleted: deletedFieldMetadataOptions, + updated: updatedFieldMetadataOptions, + created: createdFieldMetadataOptions, + } = compareTwoFlatFieldMetadataEnumOptions({ + compareLabel: false, + fromOptions: fromFlatFieldMetadata.options, + toOptions: update.to, + }); + + const flatViewGroups = findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: fromFlatFieldMetadata.viewGroupIds, + flatEntityMaps: flatViewGroupMaps, + }); + + if (flatViewGroups.length === 0) { + return { + flatViewGroupsToCreate: [], + flatViewGroupsToDelete: [], + flatViewGroupsToUpdate: [], + }; + } + + const workspaceId = fromFlatFieldMetadata.workspaceId; + + const flatViewGroupsToDelete = deletedFieldMetadataOptions.flatMap((option) => + flatViewGroups.filter( + (flatViewGroup) => flatViewGroup.fieldValue === option.value, + ), + ); + + const flatViewGroupsToUpdate = updatedFieldMetadataOptions.flatMap( + ({ from: fromOption, to: toOption }) => + flatViewGroups.flatMap((flatViewGroup) => + flatViewGroup.fieldValue === fromOption.value + ? { ...flatViewGroup, fieldValue: toOption.value } + : [], + ), + ); + + const viewGroupsByViewId = reduceFlatViewGroupsByViewId({ + flatViewGroups: flatViewGroups.filter( + (flatViewGroup) => + !flatViewGroupsToDelete.some( + (flatViewGroupToDelete) => + flatViewGroupToDelete.id === flatViewGroup.id, + ), + ), + }); + + const viewIds = Object.keys(viewGroupsByViewId.flatViewGroupRecordByViewId); + + const createdAt = new Date(); + const flatViewGroupsToCreate = createdFieldMetadataOptions.flatMap( + (option, createdOptionIndex) => + viewIds.map((viewId) => { + const viewGroupHighestPosition = + viewGroupsByViewId.highestViewGroupPositionByViewId[viewId]; + + if (!isDefined(viewGroupHighestPosition)) { + throw new FlatEntityMapsException( + 'View id highest position not found, should never occur', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const viewGroupId = v4(); + + return { + id: viewGroupId, + fieldMetadataId: fromFlatFieldMetadata.id, + viewId, + workspaceId, + createdAt: createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: viewGroupId, + isVisible: true, + fieldValue: option.value, + position: viewGroupHighestPosition + createdOptionIndex + 1, + }; + }), + ); + + return { + flatViewGroupsToCreate, + flatViewGroupsToDelete, + flatViewGroupsToUpdate, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util.ts new file mode 100644 index 0000000000..7f0818d389 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/utils/reduce-flat-view-groups-by-view-id.util.ts @@ -0,0 +1,39 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type'; + +type FlatViewGroupsByViewId = { + flatViewGroupRecordByViewId: Record>; + highestViewGroupPositionByViewId: Record; +}; +export const reduceFlatViewGroupsByViewId = ({ + flatViewGroups, +}: { + flatViewGroups: FlatViewGroup[]; +}): FlatViewGroupsByViewId => { + const initialAccumulator: FlatViewGroupsByViewId = { + flatViewGroupRecordByViewId: {}, + highestViewGroupPositionByViewId: {}, + }; + + return flatViewGroups.reduce((accumulator, flatViewGroup) => { + const accumulatorHighestPosition = + accumulator.highestViewGroupPositionByViewId[flatViewGroup.viewId]; + + return { + flatViewGroupRecordByViewId: { + ...accumulator.flatViewGroupRecordByViewId, + [flatViewGroup.viewId]: { + ...accumulator.flatViewGroupRecordByViewId[flatViewGroup.viewId], + [flatViewGroup.id]: flatViewGroup, + }, + }, + highestViewGroupPositionByViewId: { + ...accumulator.highestViewGroupPositionByViewId, + [flatViewGroup.viewId]: isDefined(accumulatorHighestPosition) + ? Math.max(accumulatorHighestPosition, flatViewGroup.position) + : flatViewGroup.position, + }, + }; + }, initialAccumulator); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index e3a47c8c6a..03f679b6f3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -2,14 +2,9 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; @@ -86,76 +81,34 @@ export class ObjectMetadataServiceV2 { flatViewMaps: existingFlatViewMaps, }); - const toFlatObjectMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatObjectMetadata, - flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, - }); - - const toFlatFieldMetadataMaps = otherObjectFlatFieldMetadatas.reduce( - (flatFieldMaps, flatFieldMetadata) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatFieldMetadata, - flatEntityMaps: flatFieldMaps, - }), - getSubFlatEntityMapsOrThrow({ - flatEntityIds: otherObjectFlatFieldMetadatas.map(({ id }) => id), - flatEntityMaps: existingFlatFieldMetadataMaps, - }), - ); - - const toFlatIndexMaps = flatIndexMetadataToUpdate.reduce( - (flatIndexMaps, flatIndexMetadata) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatIndexMetadata, - flatEntityMaps: flatIndexMaps, - }), - getSubFlatEntityMapsOrThrow({ - flatEntityIds: flatIndexMetadataToUpdate.map(({ id }) => id), - flatEntityMaps: existingFlatIndexMaps, - }), - ); - - const flatViewFieldMapsWithUpdated = flatViewFieldToUpdate.reduce( - (flatViewFieldMaps, flatViewField) => - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatViewField, - flatEntityMaps: flatViewFieldMaps, - }), - getSubFlatEntityMapsOrThrow({ - flatEntityIds: flatViewFieldToUpdate.map(({ id }) => id), - flatEntityMaps: existingFlatViewFieldMaps, - }), - ); - - const toFlatViewFieldMaps = flatViewFieldToCreate.reduce( - (flatViewFieldMaps, flatViewField) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewField, - flatEntityMaps: flatViewFieldMaps, - }), - flatViewFieldMapsWithUpdated, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatObjectMetadataMaps: { - from: existingFlatObjectMetadataMaps, - to: toFlatObjectMetadataMaps, - }, - flatIndexMaps: { - from: existingFlatIndexMaps, - to: toFlatIndexMaps, - }, - flatFieldMetadataMaps: { - from: existingFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, - flatViewFieldMaps: { - from: existingFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, + flatObjectMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatObjectMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatObjectMetadata], + }), + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: flatIndexMetadataToUpdate, + }), + flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: otherObjectFlatFieldMetadatas, + }), + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: flatViewFieldToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: flatViewFieldToUpdate, + }), }, dependencyAllFlatEntityMaps: { flatViewMaps: existingFlatViewMaps, @@ -240,70 +193,28 @@ export class ObjectMetadataServiceV2 { deleteObjectInput, }); - const impactedObjectMetadataIds = Array.from( - new Set( - flatFieldMetadatasToDelete.map( - (flatFieldMetadata) => flatFieldMetadata.objectMetadataId, - ), - ), - ); - - const fromFlatObjectMetadataMaps = getSubFlatEntityMapsOrThrow({ - flatEntityMaps: existingFlatObjectMetadataMaps, - flatEntityIds: impactedObjectMetadataIds, - }); - - const toFlatObjectMetadataMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatObjectMetadataToDelete.id, - flatEntityMaps: fromFlatObjectMetadataMaps, - }); - - const fromFlatFieldMetadataMaps = getSubFlatEntityMapsOrThrow({ - flatEntityMaps: existingFlatFieldMetadataMaps, - flatEntityIds: flatFieldMetadatasToDelete.map( - (flatField) => flatField.id, - ), - }); - - const toFlatFieldMetadataMaps = flatFieldMetadatasToDelete.reduce( - (flatFieldMaps, flatField) => - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatField.id, - flatEntityMaps: flatFieldMaps, - }), - fromFlatFieldMetadataMaps, - ); - - const fromFlatIndexMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: flatIndexToDelete.map((flatIndex) => flatIndex.id), - flatEntityMaps: existingFlatIndexMaps, - }); - - const toFlatIndexMaps = flatIndexToDelete.reduce( - (flatIndexMaps, flatIndex) => - deleteFlatEntityFromFlatEntityMapsOrThrow({ - entityToDeleteId: flatIndex.id, - flatEntityMaps: flatIndexMaps, - }), - fromFlatIndexMaps, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatObjectMetadataMaps: { - from: fromFlatObjectMetadataMaps, - to: toFlatObjectMetadataMaps, - }, - flatIndexMaps: { - from: fromFlatIndexMaps, - to: toFlatIndexMaps, - }, - flatFieldMetadataMaps: { - from: fromFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, + flatObjectMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatObjectMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: [flatObjectMetadataToDelete], + flatEntityToUpdate: [], + }), + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: [], + flatEntityToDelete: flatIndexToDelete, + flatEntityToUpdate: [], + }), + flatFieldMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: [], + flatEntityToDelete: flatFieldMetadatasToDelete, + flatEntityToUpdate: [], + }), }, buildOptions: { inferDeletionFromMissingEntities: { @@ -366,21 +277,14 @@ export class ObjectMetadataServiceV2 { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, }); - const toFlatFieldMetadataMaps = [ - ...flatFieldMetadataToCreateOnObject, - ...relationTargetFlatFieldMetadataToCreate, - ].reduce>( - (flatFieldMaps, flatField) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatField, - flatEntityMaps: flatFieldMaps, - }), - EMPTY_FLAT_ENTITY_MAPS, - ); - - const toFlatObjectMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatObjectMetadataToCreate, - flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, + const flatFieldMetadataMapsFromTo = computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatFieldMetadataMaps, + flatEntityToCreate: [ + ...flatFieldMetadataToCreateOnObject, + ...relationTargetFlatFieldMetadataToCreate, + ], + flatEntityToDelete: [], + flatEntityToUpdate: [], }); const flatDefaultViewToCreate = await this.createDefaultFlatView({ @@ -388,16 +292,11 @@ export class ObjectMetadataServiceV2 { workspaceId, }); - const toFlatViewMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatDefaultViewToCreate, - flatEntityMaps: existingFlatViewMaps, - }); - const flatDefaultViewFieldsToCreate = await this.createDefaultFlatViewFields({ objectFlatFieldMetadatas: findManyFlatEntityByIdInFlatEntityMapsOrThrow( { - flatEntityMaps: toFlatFieldMetadataMaps, + flatEntityMaps: flatFieldMetadataMapsFromTo.to, flatEntityIds: flatFieldMetadataToCreateOnObject.map( ({ id }) => id, ), @@ -407,48 +306,35 @@ export class ObjectMetadataServiceV2 { workspaceId, }); - const toFlatViewFieldMaps = flatDefaultViewFieldsToCreate.reduce( - (acc, flatViewField) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewField, - flatEntityMaps: acc, - }), - existingFlatViewFieldMaps, - ); - - const toFlatIndexMaps = flatIndexMetadataToCreate.reduce( - (flatIndexMaps, flatIndexMetadata) => - addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatIndexMetadata, - flatEntityMaps: flatIndexMaps, - }), - existingFlatIndexMaps, - ); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatObjectMetadataMaps: { - from: existingFlatObjectMetadataMaps, - to: toFlatObjectMetadataMaps, - }, - flatViewMaps: { - from: existingFlatViewMaps, - to: toFlatViewMaps, - }, - flatViewFieldMaps: { - from: existingFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, - flatFieldMetadataMaps: { - from: existingFlatFieldMetadataMaps, - to: toFlatFieldMetadataMaps, - }, - flatIndexMaps: { - from: existingFlatIndexMaps, - to: toFlatIndexMaps, - }, + flatObjectMetadataMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatObjectMetadataMaps, + flatEntityToCreate: [flatObjectMetadataToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + flatViewMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewMaps, + flatEntityToCreate: [flatDefaultViewToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: flatDefaultViewFieldsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), + flatFieldMetadataMaps: flatFieldMetadataMapsFromTo, + flatIndexMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatIndexMaps, + flatEntityToCreate: flatIndexMetadataToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, buildOptions: { isSystemBuild: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts index a58a38c9e8..5d5039e043 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts @@ -28,6 +28,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.UUID, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: true, objectMetadataId, @@ -47,6 +48,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: true, isUIReadOnly: true, defaultValue: 'uuid', + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -62,6 +64,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.TEXT, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -81,6 +84,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: false, isUIReadOnly: false, defaultValue: "''", + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -96,6 +100,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.DATE_TIME, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -115,6 +120,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: false, isUIReadOnly: true, defaultValue: 'now', + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -130,6 +136,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.DATE_TIME, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -149,6 +156,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: false, isUIReadOnly: true, defaultValue: 'now', + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -164,6 +172,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.DATE_TIME, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -183,6 +192,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: false, isUIReadOnly: true, defaultValue: null, + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -198,6 +208,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.ACTOR, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -217,7 +228,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: false, isUIReadOnly: true, defaultValue: { name: "''", source: "'MANUAL'" }, - + viewFilterIds: [], createdAt, updatedAt: createdAt, options: null, @@ -232,6 +243,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ type: FieldMetadataType.POSITION, id: v4(), viewFieldIds: [], + viewGroupIds: [], isLabelSyncedWithName: false, isUnique: false, objectMetadataId, @@ -251,6 +263,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: true, isUIReadOnly: true, defaultValue: 0, + viewFilterIds: [], createdAt, updatedAt: createdAt, @@ -265,6 +278,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ const searchVectorField: FlatFieldMetadata = { type: FieldMetadataType.TS_VECTOR, viewFieldIds: [], + viewGroupIds: [], id: v4(), isLabelSyncedWithName: false, isUnique: false, @@ -285,6 +299,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ isSystem: true, isUIReadOnly: true, defaultValue: null, + viewFilterIds: [], createdAt, updatedAt: createdAt, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts index fdc130cbf5..883d4a2727 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util.ts @@ -63,7 +63,9 @@ const generateSourceFlatFieldMetadata = ({ } return { + viewFilterIds: [], viewFieldIds: [], + viewGroupIds: [], createdAt, updatedAt: createdAt, defaultValue: null, @@ -129,6 +131,8 @@ const generateTargetFlatFieldMetadata = ({ return { morphId: null, viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], id: sourceFlatFieldMetadata.relationTargetFieldMetadataId, name: sourceFlatObjectMetadata.nameSingular, label: sourceFlatObjectMetadata.labelSingular, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts index 411ebd52b2..2227b62509 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts @@ -3,11 +3,8 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input'; import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input'; import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input'; @@ -48,20 +45,17 @@ export class RouteTriggerV2Service { workspaceId, }); - const toFlatRouteMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatRouteTriggerToCreate, - flatEntityMaps: existingFlatRouteMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatRouteTriggerMaps: { - from: existingFlatRouteMaps, - to: toFlatRouteMaps, - }, + flatRouteTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRouteMaps, + flatEntityToCreate: [flatRouteTriggerToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -114,24 +108,17 @@ export class RouteTriggerV2Service { updateRouteTriggerInput: routeTriggerInput, }); - const fromFlatRouteMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [optimisticallyUpdatedFlatRouteTrigger.id], - flatEntityMaps: existingFlatRouteMaps, - }); - const toFlatRouteMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatRouteTrigger, - flatEntityMaps: fromFlatRouteMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatRouteTriggerMaps: { - from: existingFlatRouteMaps, - to: toFlatRouteMaps, - }, + flatRouteTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRouteMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatRouteTrigger], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: @@ -192,23 +179,16 @@ export class RouteTriggerV2Service { ); } - const fromFlatRouteMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingFlatRoute.id], - flatEntityMaps: existingFlatRouteMaps, - }); - const toFlatRouteMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatRouteMaps, - entityToDeleteId: existingFlatRoute.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatRouteTriggerMaps: { - from: fromFlatRouteMaps, - to: toFlatRouteMaps, - }, + flatRouteTriggerMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatRouteMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatRoute], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatServerlessFunctionMaps: existingFlatServerlessFunctionMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts index 56de15a3d4..2e3b6bee22 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service.ts @@ -3,11 +3,8 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import type { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; @@ -54,20 +51,17 @@ export class ServerlessFunctionV2Service { workspaceId, }); - const toFlatServerlessFunctionMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatServerlessFunctionToCreate, - flatEntityMaps: existingFlatServerlessFunctionMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatServerlessFunctionMaps: { - from: existingFlatServerlessFunctionMaps, - to: toFlatServerlessFunctionMaps, - }, + flatServerlessFunctionMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatServerlessFunctionMaps, + flatEntityToCreate: [flatServerlessFunctionToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, buildOptions: { isSystemBuild: false, @@ -119,25 +113,17 @@ export class ServerlessFunctionV2Service { updateServerlessFunctionInput: serverlessFunctionInput, }); - const fromFlatServerlessFunctionMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [optimisticallyUpdatedFlatServerlessFunction.id], - flatEntityMaps: existingFlatServerlessFunctionMaps, - }); - const toFlatServerlessFunctionMaps = - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatServerlessFunction, - flatEntityMaps: fromFlatServerlessFunctionMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { workspaceId, fromToAllFlatEntityMaps: { - flatServerlessFunctionMaps: { - from: existingFlatServerlessFunctionMaps, - to: toFlatServerlessFunctionMaps, - }, + flatServerlessFunctionMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatServerlessFunctionMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatServerlessFunction], + }), }, buildOptions: { isSystemBuild: false, @@ -200,20 +186,18 @@ export class ServerlessFunctionV2Service { deletedAt: new Date(), }; - const toFlatServerlessFunctionMaps = - replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt, - flatEntityMaps: existingFlatServerlessFunctionMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatServerlessFunctionMaps: { - from: existingFlatServerlessFunctionMaps, - to: toFlatServerlessFunctionMaps, - }, + flatServerlessFunctionMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatServerlessFunctionMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatServerlessFunctionWithDeletedAt, + ], + }), }, buildOptions: { isSystemBuild, @@ -274,24 +258,16 @@ export class ServerlessFunctionV2Service { ); } - const fromFlatServerlessFunctionMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingFlatServerlessFunction.id], - flatEntityMaps: existingFlatServerlessFunctionMaps, - }); - const toFlatServerlessFunctionMaps = - deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatServerlessFunctionMaps, - entityToDeleteId: existingFlatServerlessFunction.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatServerlessFunctionMaps: { - from: fromFlatServerlessFunctionMaps, - to: toFlatServerlessFunctionMaps, - }, + flatServerlessFunctionMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatServerlessFunctionMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatServerlessFunction], + flatEntityToUpdate: [], + }), }, buildOptions: { isSystemBuild, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts index 945c65225e..e3551def33 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts @@ -2,13 +2,9 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util'; import { fromDeleteViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/metadata-modules/flat-view-field/utils/from-delete-view-field-input-to-flat-view-field-or-throw.util'; import { fromDestroyViewFieldInputToFlatViewFieldOrThrow } from 'src/engine/metadata-modules/flat-view-field/utils/from-destroy-view-field-input-to-flat-view-field-or-throw.util'; @@ -58,19 +54,16 @@ export class ViewFieldV2Service { workspaceId, }); - const toFlatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewFieldToCreate, - flatEntityMaps: existingFlatViewFieldMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFieldMaps: { - from: existingFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: [flatViewFieldToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatFieldMetadataMaps, @@ -133,19 +126,16 @@ export class ViewFieldV2Service { updateViewFieldInput, }); - const toFlatViewFieldMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatView, - flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFieldMaps: { - from: existingFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatView], + }), }, dependencyAllFlatEntityMaps: { flatObjectMetadataMaps, @@ -207,19 +197,16 @@ export class ViewFieldV2Service { deleteViewFieldInput, }); - const toFlatViewFieldMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatViewWithDeletedAt, - flatEntityMaps: existingFlatViewFieldMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFieldMaps: { - from: existingFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewWithDeletedAt], + }), }, dependencyAllFlatEntityMaps: { flatObjectMetadataMaps, @@ -277,23 +264,16 @@ export class ViewFieldV2Service { flatViewFieldMaps: existingFlatViewFieldMaps, }); - const fromFlatViewFieldMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingViewFieldToDelete.id], - flatEntityMaps: existingFlatViewFieldMaps, - }); - const toFlatViewFieldMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatViewFieldMaps, - entityToDeleteId: existingViewFieldToDelete.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFieldMaps: { - from: fromFlatViewFieldMaps, - to: toFlatViewFieldMaps, - }, + flatViewFieldMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFieldMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingViewFieldToDelete], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatViewMaps: existingFlatViewMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts index 98e9164424..74c23a49b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/services/view-filter-v2.service.ts @@ -2,13 +2,9 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { fromCreateViewFilterInputToFlatViewFilterToCreate } from 'src/engine/metadata-modules/flat-view-filter/utils/from-create-view-filter-input-to-flat-view-filter-to-create.util'; import { fromDeleteViewFilterInputToFlatViewFilterOrThrow } from 'src/engine/metadata-modules/flat-view-filter/utils/from-delete-view-filter-input-to-flat-view-filter-or-throw.util'; import { fromDestroyViewFilterInputToFlatViewFilterOrThrow } from 'src/engine/metadata-modules/flat-view-filter/utils/from-destroy-view-filter-input-to-flat-view-filter-or-throw.util'; @@ -58,19 +54,16 @@ export class ViewFilterV2Service { workspaceId, }); - const toFlatViewFilterMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewFilterToCreate, - flatEntityMaps: existingFlatViewFilterMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFilterMaps: { - from: existingFlatViewFilterMaps, - to: toFlatViewFilterMaps, - }, + flatViewFilterMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFilterMaps, + flatEntityToCreate: [flatViewFilterToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatFieldMetadataMaps, @@ -133,19 +126,16 @@ export class ViewFilterV2Service { updateViewFilterInput, }); - const toFlatViewFilterMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatViewFilter, - flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFilterMaps: { - from: existingFlatViewFilterMaps, - to: toFlatViewFilterMaps, - }, + flatViewFilterMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFilterMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewFilter], + }), }, dependencyAllFlatEntityMaps: { flatViewMaps, @@ -207,19 +197,18 @@ export class ViewFilterV2Service { deleteViewFilterInput, }); - const toFlatViewFilterMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatViewFilterWithDeletedAt, - flatEntityMaps: existingFlatViewFilterMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFilterMaps: { - from: existingFlatViewFilterMaps, - to: toFlatViewFilterMaps, - }, + flatViewFilterMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFilterMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatViewFilterWithDeletedAt, + ], + }), }, dependencyAllFlatEntityMaps: { flatFieldMetadataMaps, @@ -281,23 +270,16 @@ export class ViewFilterV2Service { flatViewFilterMaps: existingFlatViewFilterMaps, }); - const fromFlatViewFilterMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingViewFilterToDelete.id], - flatEntityMaps: existingFlatViewFilterMaps, - }); - const toFlatViewFilterMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatViewFilterMaps, - entityToDeleteId: existingViewFilterToDelete.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewFilterMaps: { - from: fromFlatViewFilterMaps, - to: toFlatViewFilterMaps, - }, + flatViewFilterMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewFilterMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingViewFilterToDelete], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatViewMaps: existingFlatViewMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts index 29beb5d62d..d904eb0e6f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-group/services/view-group-v2.service.ts @@ -2,13 +2,9 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { EMPTY_FLAT_ENTITY_MAPS } from 'src/engine/metadata-modules/flat-entity/constant/empty-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { fromCreateViewGroupInputToFlatViewGroupToCreate } from 'src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util'; import { fromDeleteViewGroupInputToFlatViewGroupOrThrow } from 'src/engine/metadata-modules/flat-view-group/utils/from-delete-view-group-input-to-flat-view-group-or-throw.util'; import { fromDestroyViewGroupInputToFlatViewGroupOrThrow } from 'src/engine/metadata-modules/flat-view-group/utils/from-destroy-view-group-input-to-flat-view-group-or-throw.util'; @@ -56,19 +52,16 @@ export class ViewGroupV2Service { workspaceId, }); - const toFlatViewGroupMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewGroupToCreate, - flatEntityMaps: existingFlatViewGroupMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewGroupMaps: { - from: existingFlatViewGroupMaps, - to: toFlatViewGroupMaps, - }, + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: [flatViewGroupToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatFieldMetadataMaps, @@ -130,19 +123,16 @@ export class ViewGroupV2Service { updateViewGroupInput, }); - const toFlatViewGroupMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatViewGroup, - flatEntityMaps: EMPTY_FLAT_ENTITY_MAPS, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewGroupMaps: { - from: existingFlatViewGroupMaps, - to: toFlatViewGroupMaps, - }, + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewGroup], + }), }, dependencyAllFlatEntityMaps: { flatViewMaps, @@ -204,19 +194,18 @@ export class ViewGroupV2Service { deleteViewGroupInput, }); - const toFlatViewGroupMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: optimisticallyUpdatedFlatViewGroupWithDeletedAt, - flatEntityMaps: existingFlatViewGroupMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewGroupMaps: { - from: existingFlatViewGroupMaps, - to: toFlatViewGroupMaps, - }, + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + optimisticallyUpdatedFlatViewGroupWithDeletedAt, + ], + }), }, dependencyAllFlatEntityMaps: { flatFieldMetadataMaps, @@ -278,23 +267,16 @@ export class ViewGroupV2Service { flatViewGroupMaps: existingFlatViewGroupMaps, }); - const fromFlatViewGroupMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [existingViewGroupToDelete.id], - flatEntityMaps: existingFlatViewGroupMaps, - }); - const toFlatViewGroupMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatViewGroupMaps, - entityToDeleteId: existingViewGroupToDelete.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewGroupMaps: { - from: fromFlatViewGroupMaps, - to: toFlatViewGroupMaps, - }, + flatViewGroupMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewGroupMaps, + flatEntityToCreate: [], + flatEntityToDelete: [existingViewGroupToDelete], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatViewMaps: existingFlatViewMaps, diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts index cfb8c2f8e7..0abaeef554 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view-v2.service.ts @@ -3,11 +3,8 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; -import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util'; -import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util'; import { fromDeleteViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-delete-view-input-to-flat-view-or-throw.util'; import { fromDestroyViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-destroy-view-input-to-flat-view-or-throw.util'; @@ -47,19 +44,16 @@ export class ViewV2Service { workspaceId, }); - const toFlatViewMaps = addFlatEntityToFlatEntityMapsOrThrow({ - flatEntity: flatViewFromCreateInput, - flatEntityMaps: existingFlatViewMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewMaps: { - from: existingFlatViewMaps, - to: toFlatViewMaps, - }, + flatViewMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewMaps, + flatEntityToCreate: [flatViewFromCreateInput], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }), }, dependencyAllFlatEntityMaps: { flatObjectMetadataMaps: flatObjectMetadataMaps, @@ -113,23 +107,16 @@ export class ViewV2Service { flatViewMaps: existingFlatViewMaps, }); - const fromFlatViewMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [flatViewFromUpdateInput.id], - flatEntityMaps: existingFlatViewMaps, - }); - const toFlatViewMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatViewFromUpdateInput, - flatEntityMaps: fromFlatViewMaps, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewMaps: { - from: fromFlatViewMaps, - to: toFlatViewMaps, - }, + flatViewMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [flatViewFromUpdateInput], + }), }, buildOptions: { isSystemBuild: false, @@ -174,28 +161,22 @@ export class ViewV2Service { }, ); - const flatViewFromDeleteInput = fromDeleteViewInputToFlatViewOrThrow({ - deleteViewInput, - flatViewMaps: existingFlatViewMaps, - }); - - const fromFlatViewMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [flatViewFromDeleteInput.id], - flatEntityMaps: existingFlatViewMaps, - }); - const toFlatViewMaps = replaceFlatEntityInFlatEntityMapsOrThrow({ - flatEntity: flatViewFromDeleteInput, - flatEntityMaps: fromFlatViewMaps, - }); + const optimisticallyUpdatedFlatViewWithDeletedAt = + fromDeleteViewInputToFlatViewOrThrow({ + deleteViewInput, + flatViewMaps: existingFlatViewMaps, + }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewMaps: { - from: fromFlatViewMaps, - to: toFlatViewMaps, - }, + flatViewMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewMaps, + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [optimisticallyUpdatedFlatViewWithDeletedAt], + }), }, buildOptions: { isSystemBuild: false, @@ -245,23 +226,16 @@ export class ViewV2Service { flatViewMaps: existingFlatViewMaps, }); - const fromFlatViewMaps = getSubFlatEntityMapsOrThrow({ - flatEntityIds: [flatViewFromDestroyInput.id], - flatEntityMaps: existingFlatViewMaps, - }); - const toFlatViewMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({ - flatEntityMaps: fromFlatViewMaps, - entityToDeleteId: flatViewFromDestroyInput.id, - }); - const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { fromToAllFlatEntityMaps: { - flatViewMaps: { - from: fromFlatViewMaps, - to: toFlatViewMaps, - }, + flatViewMaps: computeFlatEntityMapsFromTo({ + flatEntityMaps: existingFlatViewMaps, + flatEntityToCreate: [], + flatEntityToDelete: [flatViewFromDestroyInput], + flatEntityToUpdate: [], + }), }, buildOptions: { isSystemBuild: false, diff --git a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-enum-manager.service.ts b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-enum-manager.service.ts index 92f37ac4a3..bad3d3f553 100644 --- a/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-enum-manager.service.ts +++ b/packages/twenty-server/src/engine/twenty-orm/workspace-schema-manager/services/workspace-schema-enum-manager.service.ts @@ -213,6 +213,8 @@ export class WorkspaceSchemaEnumManagerService { oldColumnName, newColumnName: columnName, oldToNewEnumOptionMap, + columnDefinition, + oldEnumTypeName: oldEnumName, }); } @@ -315,49 +317,116 @@ export class WorkspaceSchemaEnumManagerService { oldColumnName, newColumnName, oldToNewEnumOptionMap, + columnDefinition, + oldEnumTypeName, }: { queryRunner: QueryRunner; schemaName: string; tableName: string; oldColumnName: string; newColumnName: string; + oldEnumTypeName: string; oldToNewEnumOptionMap: Record; + columnDefinition: WorkspaceSchemaColumnDefinition; }): Promise { - const safeSchemaName = removeSqlDDLInjection(schemaName); - const safeTableName = removeSqlDDLInjection(tableName); - const safeOldColumnName = removeSqlDDLInjection(oldColumnName); - const safeNewColumnName = removeSqlDDLInjection(newColumnName); - const newEnumTypeName = computePostgresEnumName({ tableName, columnName: newColumnName, }); - if (Object.keys(oldToNewEnumOptionMap).length === 0) { - return; - } - + const safeSchemaName = removeSqlDDLInjection(schemaName); + const safeTableName = removeSqlDDLInjection(tableName); + const safeOldColumnName = removeSqlDDLInjection(oldColumnName); + const safeNewColumnName = removeSqlDDLInjection(newColumnName); const caseStatements = Object.entries(oldToNewEnumOptionMap) .map( ([oldEnumOption, newEnumOption]) => `WHEN '${removeSqlDDLInjection(oldEnumOption)}' THEN '${removeSqlDDLInjection(newEnumOption)}'::"${safeSchemaName}"."${newEnumTypeName}"`, ) .join(' '); - const mappedValuesCondition = Object.keys(oldToNewEnumOptionMap) .map((oldValue) => `'${removeSqlDDLInjection(oldValue)}'`) .join(', '); - // Update rows with mapped enum values - const updateMappedSql = ` - UPDATE "${safeSchemaName}"."${safeTableName}" - SET "${safeNewColumnName}" = - CASE "${safeOldColumnName}"::text - ${caseStatements} - END - WHERE "${safeOldColumnName}" IS NOT NULL - AND "${safeOldColumnName}"::text IN (${mappedValuesCondition})`; + const sqlQuery = columnDefinition.isArray + ? this.updateArrayEnum({ + safeSchemaName, + safeTableName, + safeOldColumnName, + safeNewColumnName, + newEnumTypeName, + oldEnumTypeName, + caseStatements, + mappedValuesCondition, + }) + : this.updateAtomicEnum({ + safeSchemaName, + safeTableName, + safeOldColumnName, + safeNewColumnName, + caseStatements, + mappedValuesCondition, + }); - await queryRunner.query(updateMappedSql); + await queryRunner.query(sqlQuery); + } + + private updateArrayEnum({ + safeNewColumnName, + safeOldColumnName, + safeSchemaName, + safeTableName, + newEnumTypeName, + oldEnumTypeName, + caseStatements, + mappedValuesCondition, + }: { + safeSchemaName: string; + safeTableName: string; + safeOldColumnName: string; + safeNewColumnName: string; + newEnumTypeName: string; + oldEnumTypeName: string; + caseStatements: string; + mappedValuesCondition: string; + }) { + return ` + UPDATE "${safeSchemaName}"."${safeTableName}" + SET "${safeNewColumnName}" = ( + SELECT array_agg( + CASE unnest_value::text + ${caseStatements} + ELSE unnest_value::text::"${safeSchemaName}"."${newEnumTypeName}" + END + ) + FROM unnest("${safeOldColumnName}") AS unnest_value + ) + WHERE "${safeOldColumnName}" IS NOT NULL + AND "${safeOldColumnName}" && ARRAY[${mappedValuesCondition}]::"${safeSchemaName}"."${oldEnumTypeName}"[]`; + } + + private updateAtomicEnum({ + safeNewColumnName, + safeOldColumnName, + safeSchemaName, + safeTableName, + caseStatements, + mappedValuesCondition, + }: { + caseStatements: string; + mappedValuesCondition: string; + safeSchemaName: string; + safeTableName: string; + safeOldColumnName: string; + safeNewColumnName: string; + }) { + return ` + UPDATE "${safeSchemaName}"."${safeTableName}" + SET "${safeNewColumnName}" = + CASE "${safeOldColumnName}"::text + ${caseStatements} + END + WHERE "${safeOldColumnName}" IS NOT NULL + AND "${safeOldColumnName}"::text IN (${mappedValuesCondition})`; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service.ts index 81915da399..c55dad0078 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-filter/workspace-migration-v2-view-filter-actions-builder.service.ts @@ -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, }, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service.ts index e27d8d282f..67c64b536a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-group/workspace-migration-v2-view-group-actions-builder.service.ts @@ -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, }, }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service.ts index e4aa58689a..bc7ef8fad9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-filter-validator.service.ts @@ -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 { @@ -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`, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts index 8835fe1ca6..93778e58ab 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-group-validator.service.ts @@ -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 { @@ -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; diff --git a/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts index 463e6f3001..a9ec4b4120 100644 --- a/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts +++ b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts @@ -19,7 +19,9 @@ export const getMockFieldMetadataEntity = < overrides: GetMockFieldMetadataEntityOverride, ): FieldMetadataEntity => { return { + viewFilters: [], viewFields: [], + viewGroups: [], morphId: null, fieldPermissions: [], icon: null, diff --git a/packages/twenty-server/test/integration/graphql/suites/group-by-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/group-by-resolvers.integration-spec.ts index 55891779b0..cba1e7f607 100644 --- a/packages/twenty-server/test/integration/graphql/suites/group-by-resolvers.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/group-by-resolvers.integration-spec.ts @@ -4,12 +4,12 @@ import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-field import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; import { createViewFilterGroupOperationFactory } from 'test/integration/graphql/utils/create-view-filter-group-operation-factory.util'; -import { createViewOperationFactory } from 'test/integration/graphql/utils/create-view-operation-factory.util'; import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util'; import { groupByOperationFactory } from 'test/integration/graphql/utils/group-by-operation-factory.util'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; import { createOneCoreViewFilter } from 'test/integration/metadata/suites/view-filter/utils/create-one-core-view-filter.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; import { jestExpectToBeDefined } from 'test/utils/expect-to-be-defined.util.test'; import { ViewFilterOperand } from 'twenty-shared/types'; @@ -419,17 +419,16 @@ describe('group-by resolvers (integration)', () => { ); // create a view with a filter: city eq cityToKeep - const createViewResponse = await makeGraphqlAPIRequest( - createViewOperationFactory({ - data: { - name: 'People View City Keep', - objectMetadataId: personObjectMetadataId, - icon: 'Icon123', - }, - }), - ); + const { data: createViewData } = await createOneCoreView({ + input: { + name: 'People View City Keep', + objectMetadataId: personObjectMetadataId, + icon: 'Icon123', + }, + expectToFail: false, + }); - viewId = createViewResponse.body.data.createCoreView.id as string; + viewId = createViewData.createCoreView.id; // create a filter group and a filter for the view const viewFilterGroupResponse = await makeGraphqlAPIRequest( @@ -510,18 +509,17 @@ describe('group-by resolvers (integration)', () => { ); // create a view with any field filter - const createViewResponse = await makeGraphqlAPIRequest( - createViewOperationFactory({ - data: { - name: 'People View City Keep', - objectMetadataId: personObjectMetadataId, - icon: 'Icon123', - anyFieldFilterValue: cityA, - }, - }), - ); + const { data: createViewData } = await createOneCoreView({ + input: { + name: 'People View City Keep', + objectMetadataId: personObjectMetadataId, + icon: 'Icon123', + anyFieldFilterValue: cityA, + }, + expectToFail: false, + }); - viewId = createViewResponse.body.data.createCoreView.id as string; + viewId = createViewData.createCoreView.id; const response = await makeGraphqlAPIRequest( groupByOperationFactory({ diff --git a/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts index 4f05844628..60dfae0cfd 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/create-view.integration-spec.ts @@ -1,9 +1,7 @@ -import { createViewOperationFactory } from 'test/integration/graphql/utils/create-view-operation-factory.util'; -import { assertGraphQLSuccessfulResponse } from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; import { assertViewStructure, cleanupViewRecords, @@ -56,30 +54,29 @@ describe('Create core view', () => { }); it('should create a new view with all properties', async () => { - const input = { + const { data, errors } = await createOneCoreView({ + input: { + name: 'Kanban View', + objectMetadataId: testObjectMetadataId, + icon: 'IconDeal', + type: ViewType.KANBAN, + position: 1, + isCompact: true, + openRecordIn: ViewOpenRecordIn.SIDE_PANEL, + }, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + assertViewStructure(data.createCoreView, { name: 'Kanban View', objectMetadataId: testObjectMetadataId, - icon: 'IconDeal', type: ViewType.KANBAN, key: null, + icon: 'IconDeal', position: 1, isCompact: true, openRecordIn: ViewOpenRecordIn.SIDE_PANEL, - }; - - const operation = createViewOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - assertViewStructure(response.body.data.createCoreView, { - name: input.name, - objectMetadataId: input.objectMetadataId, - type: input.type, - key: null, - icon: input.icon, - position: input.position, - isCompact: input.isCompact, - openRecordIn: input.openRecordIn, }); }); @@ -90,11 +87,13 @@ describe('Create core view', () => { icon: 'IconList', }; - const operation = createViewOperationFactory({ data: input }); - const response = await makeGraphqlAPIRequest(operation); + const { data, errors } = await createOneCoreView({ + input, + expectToFail: false, + }); - assertGraphQLSuccessfulResponse(response); - assertViewStructure(response.body.data.createCoreView, { + expect(errors).toBeUndefined(); + assertViewStructure(data.createCoreView, { name: input.name, objectMetadataId: input.objectMetadataId, icon: input.icon, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/delete-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/delete-view.integration-spec.ts index c5e24628f4..d5f66b2447 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/delete-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/delete-view.integration-spec.ts @@ -1,16 +1,12 @@ import { TEST_NOT_EXISTING_VIEW_ID } from 'test/integration/constants/test-view-ids.constants'; -import { deleteViewOperationFactory } from 'test/integration/graphql/utils/delete-view-operation-factory.util'; -import { findViewOperationFactory } from 'test/integration/graphql/utils/find-view-operation-factory.util'; -import { - assertGraphQLErrorResponseWithSnapshot, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { cleanupViewRecords } from 'test/integration/utils/view-test.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { deleteOneCoreView } from 'test/integration/metadata/suites/view/utils/delete-one-core-view.util'; +import { findOneCoreView } from 'test/integration/metadata/suites/view/utils/find-one-core-view.util'; +import { cleanupViewRecords } from 'test/integration/utils/view-test.util'; describe('Delete core view', () => { let testObjectMetadataId: string; @@ -67,24 +63,28 @@ describe('Delete core view', () => { expectToFail: false, }); - const deleteOperation = deleteViewOperationFactory({ viewId: view.id }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); + const { data: deleteData, errors: deleteErrors } = await deleteOneCoreView({ + viewId: view.id, + expectToFail: false, + }); - assertGraphQLSuccessfulResponse(deleteResponse); - expect(deleteResponse.body.data.deleteCoreView).toBe(true); + expect(deleteErrors).toBeUndefined(); + expect(deleteData.deleteCoreView).toBe(true); - const getOperation = findViewOperationFactory({ viewId: view.id }); - const getResponse = await makeGraphqlAPIRequest(getOperation); + const { data: getData } = await findOneCoreView({ + viewId: view.id, + expectToFail: false, + }); - expect(getResponse.body.data.getCoreView).toBeNull(); + expect(getData.getCoreView).toBeNull(); }); it('should throw an error when deleting non-existent view', async () => { - const operation = deleteViewOperationFactory({ + const { errors } = await deleteOneCoreView({ viewId: TEST_NOT_EXISTING_VIEW_ID, + expectToFail: true, }); - const response = await makeGraphqlAPIRequest(operation); - assertGraphQLErrorResponseWithSnapshot(response); + expectOneNotInternalServerErrorSnapshot({ errors }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts index 49e04426e1..1fb826d768 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/destroy-view.integration-spec.ts @@ -1,17 +1,13 @@ import { TEST_NOT_EXISTING_VIEW_ID } from 'test/integration/constants/test-view-ids.constants'; -import { deleteViewOperationFactory } from 'test/integration/graphql/utils/delete-view-operation-factory.util'; -import { destroyViewOperationFactory } from 'test/integration/graphql/utils/destroy-view-operation-factory.util'; -import { findViewOperationFactory } from 'test/integration/graphql/utils/find-view-operation-factory.util'; -import { - assertGraphQLErrorResponseWithSnapshot, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { cleanupViewRecords } from 'test/integration/utils/view-test.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { deleteOneCoreView } from 'test/integration/metadata/suites/view/utils/delete-one-core-view.util'; +import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; +import { findOneCoreView } from 'test/integration/metadata/suites/view/utils/find-one-core-view.util'; +import { cleanupViewRecords } from 'test/integration/utils/view-test.util'; describe('Destroy core view', () => { let testObjectMetadataId: string; @@ -56,7 +52,7 @@ describe('Destroy core view', () => { await cleanupViewRecords(); }); - it('should destroy an existing view', async () => { + it.only('should destroy an existing view', async () => { const { data: { createCoreView: view }, } = await createOneCoreView({ @@ -67,29 +63,38 @@ describe('Destroy core view', () => { }, expectToFail: false, }); - const deleteOperation = deleteViewOperationFactory({ viewId: view.id }); - const deleteResponse = await makeGraphqlAPIRequest(deleteOperation); - assertGraphQLSuccessfulResponse(deleteResponse); + const { data: deleteData, errors: deleteErrors } = await deleteOneCoreView({ + viewId: view.id, + expectToFail: false, + }); - const destroyOperation = destroyViewOperationFactory({ viewId: view.id }); - const destroyResponse = await makeGraphqlAPIRequest(destroyOperation); + expect(deleteErrors).toBeUndefined(); + expect(deleteData.deleteCoreView).toBe(true); - assertGraphQLSuccessfulResponse(destroyResponse); - expect(destroyResponse.body.data.destroyCoreView).toBe(true); + const { data: destroyData, errors: destroyErrors } = + await destroyOneCoreView({ + viewId: view.id, + expectToFail: false, + }); - const getOperation = findViewOperationFactory({ viewId: view.id }); - const getResponse = await makeGraphqlAPIRequest(getOperation); + expect(destroyErrors).toBeUndefined(); + expect(destroyData.destroyCoreView).toBe(true); - expect(getResponse.body.data.getCoreView).toBeNull(); + const { data: getData } = await findOneCoreView({ + viewId: view.id, + expectToFail: false, + }); + + expect(getData.getCoreView).toBeNull(); }); it('should throw an error when destroying non-existent view', async () => { - const operation = destroyViewOperationFactory({ + const { errors } = await destroyOneCoreView({ viewId: TEST_NOT_EXISTING_VIEW_ID, + expectToFail: true, }); - const response = await makeGraphqlAPIRequest(operation); - assertGraphQLErrorResponseWithSnapshot(response); + expectOneNotInternalServerErrorSnapshot({ errors }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts index 51a1917814..794480bf36 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/update-view.integration-spec.ts @@ -1,15 +1,10 @@ import { TEST_NOT_EXISTING_VIEW_ID } from 'test/integration/constants/test-view-ids.constants'; -import { - assertGraphQLErrorResponseWithSnapshot, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { updateViewOperationFactory } from 'test/integration/graphql/utils/update-view-operation-factory.util'; -import { updateViewData } from 'test/integration/graphql/utils/view-data-factory.util'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { updateOneCoreView } from 'test/integration/metadata/suites/view/utils/update-one-core-view.util'; import { cleanupViewRecords } from 'test/integration/utils/view-test.util'; import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; @@ -71,32 +66,35 @@ describe('Update core view', () => { expectToFail: false, }); - const updateInput = updateViewData({ + const updateInput = { + id: view.id, + name: 'Updated View', + type: ViewType.KANBAN, + isCompact: true, + }; + + const { data, errors } = await updateOneCoreView({ + viewId: view.id, + input: updateInput, + expectToFail: false, + }); + + expect(errors).toBeUndefined(); + expect(data.updateCoreView).toMatchObject({ + id: view.id, name: 'Updated View', type: ViewType.KANBAN, isCompact: true, }); - - const operation = updateViewOperationFactory({ - viewId: view.id, - data: updateInput, - }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.updateCoreView).toMatchObject({ - id: view.id, - ...updateInput, - }); }); it('should throw error when updating non-existent view', async () => { - const operation = updateViewOperationFactory({ + const { errors } = await updateOneCoreView({ viewId: TEST_NOT_EXISTING_VIEW_ID, - data: { name: 'Non-existent View' }, + input: { id: TEST_NOT_EXISTING_VIEW_ID, name: 'Non-existent View' }, + expectToFail: true, }); - const response = await makeGraphqlAPIRequest(operation); - assertGraphQLErrorResponseWithSnapshot(response); + expectOneNotInternalServerErrorSnapshot({ errors }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-view-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-view-operation-factory.util.ts deleted file mode 100644 index 73806d0dda..0000000000 --- a/packages/twenty-server/test/integration/graphql/utils/create-view-operation-factory.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import gql from 'graphql-tag'; -import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; - -import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; - -type CreateViewOperationFactoryParams = { - gqlFields?: string; - data?: Partial; -}; - -export const createViewOperationFactory = ({ - gqlFields = VIEW_GQL_FIELDS, - data = {}, -}: CreateViewOperationFactoryParams = {}) => ({ - query: gql` - mutation CreateCoreView($input: CreateViewInput!) { - createCoreView(input: $input) { - ${gqlFields} - } - } - `, - variables: { - input: data, - }, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-views-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-views-operation-factory.util.ts deleted file mode 100644 index a4c81a770c..0000000000 --- a/packages/twenty-server/test/integration/graphql/utils/find-views-operation-factory.util.ts +++ /dev/null @@ -1,19 +0,0 @@ -import gql from 'graphql-tag'; -import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; - -export const findViewsOperationFactory = ({ - gqlFields = VIEW_GQL_FIELDS, - objectMetadataId, -}: { - gqlFields?: string; - objectMetadataId?: string; -} = {}) => ({ - query: gql` - query GetCoreViews($objectMetadataId: String) { - getCoreViews(objectMetadataId: $objectMetadataId) { - ${gqlFields} - } - } - `, - variables: objectMetadataId ? { objectMetadataId } : {}, -}); diff --git a/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts b/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts index 5eb4c5ac40..cb051ff4f4 100644 --- a/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/view-graphql.util.ts @@ -1,34 +1,36 @@ -import { type GraphQLResponse } from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { type CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input'; import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { createViewOperationFactory } from './create-view-operation-factory.util'; import { createViewData } from './view-data-factory.util'; -interface CreateViewResponse extends Record { - createCoreView: ViewEntity; -} - export const createTestViewWithGraphQL = async ( overrides: Partial = {}, ): Promise => { - const input = createViewData(overrides); + const viewData = createViewData(overrides); + const input: CreateViewInput = { + name: viewData.name, + objectMetadataId: viewData.objectMetadataId as string, + icon: viewData.icon, + type: viewData.type, + position: viewData.position, + isCompact: viewData.isCompact, + openRecordIn: viewData.openRecordIn, + }; - const operation = createViewOperationFactory({ data: input }); - const response = (await makeGraphqlAPIRequest( - operation, - )) as GraphQLResponse; + const { data, errors } = await createOneCoreView({ + input, + expectToFail: false, + }); - if (response.body.errors) { - throw new Error( - `Failed to create test view: ${JSON.stringify(response.body.errors)}`, - ); + if (errors) { + throw new Error(`Failed to create test view: ${JSON.stringify(errors)}`); } - if (!response.body.data) { + if (!data) { throw new Error('No data returned from createTestViewWithGraphQL'); } - return response.body.data.createCoreView; + return data.createCoreView; }; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts.snap new file mode 100644 index 0000000000..94e0399f9e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts.snap @@ -0,0 +1,221 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + "OPTION_2", + "OPTION_3", + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` +[ + { + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", + }, +] +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should update related multi selected options view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0_UPDATED", + "OPTION_1", + "OPTION_2_UPDATED", + "OPTION_3", + "OPTION_4_UPDATED", + "OPTION_5", + "OPTION_6_UPDATED", + "OPTION_7", + "OPTION_8_UPDATED", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 MULTI_SELECT should update related solo selected option view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_5_UPDATED", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should handle adding new options while maintaining existing view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + "OPTION_2", + "OPTION_3", + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should handle partial deletion of selected options in view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should handle reordering of options while maintaining view filter values 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0", + "OPTION_1", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` +[ + { + "extensions": { + "code": "INTERNAL_SERVER_ERROR", + "exceptionEventId": "mocked-exception-id", + "userFriendlyMessage": "An error occurred.", + }, + "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", + }, +] +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should update related multi selected options view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_0_UPDATED", + "OPTION_1", + "OPTION_2_UPDATED", + "OPTION_3", + "OPTION_4_UPDATED", + "OPTION_5", + "OPTION_6_UPDATED", + "OPTION_7", + "OPTION_8_UPDATED", + "OPTION_9", + ], + "viewFilterGroupId": null, +} +`; + +exports[`update-one-field-metadata-view-filters-side-effect-v2 SELECT should update related solo selected option view filter 1`] = ` +{ + "operand": "IS", + "positionInViewFilterGroup": null, + "subFieldName": null, + "value": [ + "OPTION_5_UPDATED", + ], + "viewFilterGroupId": null, +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts new file mode 100644 index 0000000000..b0dc225434 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-filters-side-effect-v2.integration-spec.ts @@ -0,0 +1,374 @@ +import { faker } from '@faker-js/faker'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createOneCoreViewFilter } from 'test/integration/metadata/suites/view-filter/utils/create-one-core-view-filter.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { findViewFilterWithRestApi } from 'test/integration/rest/utils/view-rest-api.util'; +import { + eachTestingContextFilter, + type EachTestingContext, +} from 'twenty-shared/testing'; +import { + FieldMetadataType, + ViewFilterOperand, + type EnumFieldMetadataType, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { + type FieldMetadataComplexOption, + type FieldMetadataDefaultOption, +} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type'; +import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; + +type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption; + +const generateOption = (index: number): Option => ({ + label: `Option ${index}`, + value: `OPTION_${index}`, + color: 'green', + position: index, +}); +const generateOptions = (length: number) => + Array.from({ length }, (_value, index) => generateOption(index)); +const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({ + ...option, + value: `${value}_UPDATED`, + label: `${label} updated`, +}); + +const ALL_OPTIONS = generateOptions(10); + +const isEven = (_value: unknown, index: number) => index % 2 === 0; + +type ViewFilterUpdate = { + value: string[]; +}; + +type FieldMetadataOptionsAndType = { + options: Option[]; + type: EnumFieldMetadataType; +}; + +type TestCase = EachTestingContext<{ + fieldMetadata?: FieldMetadataOptionsAndType; + createViewFilter?: ViewFilterUpdate; + updateOptions: ( + options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[], + ) => FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]; + expected?: null; +}>; +const testFieldMetadataType: EnumFieldMetadataType[] = [ + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, +]; + +describe('update-one-field-metadata-view-filters-side-effect-v2', () => { + let idToDelete: string; + + const createObjectSelectFieldAndView = async ({ + options, + type: fieldMetadataType, + }: FieldMetadataOptionsAndType) => { + const singular = faker.lorem.words(); + const plural = singular + faker.lorem.word(); + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: getMockCreateObjectInput({ + labelSingular: singular, + labelPlural: plural, + nameSingular: singular.split(' ').join(''), + namePlural: plural.split(' ').join(''), + isLabelSyncedWithName: false, + }), + }); + + idToDelete = createOneObject.id; + + const { + data: { createOneField }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + objectMetadataId: createOneObject.id, + type: fieldMetadataType, + name: 'testName', + label: 'Test name', + isLabelSyncedWithName: true, + options, + }, + gqlFields: ` + id + options + `, + }); + + const { + data: { createCoreView: createdView }, + } = await createOneCoreView({ + input: { + id: faker.string.uuid(), + icon: '123Icon', + name: 'Test View', + objectMetadataId: createOneObject.id, + type: ViewType.TABLE, + }, + expectToFail: false, + gqlFields: ` + id + `, + }); + + return { createOneObject, createOneField, createdView }; + }; + + afterEach(async () => { + if (isDefined(idToDelete)) { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: idToDelete, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: idToDelete }, + }); + } + }); + + describe.each(testFieldMetadataType)('%s', (fieldType) => { + const testCases: TestCase[] = [ + { + title: + 'should delete related view filter if all select field options got deleted', + context: { + updateOptions: () => generateOptions(3), + expected: null, + }, + }, + { + title: 'should update related multi selected options view filter', + context: { + updateOptions: (options) => + options.map((option, index) => + isEven(option, index) ? fakeOptionUpdate(option) : option, + ), + }, + }, + { + title: 'should update related solo selected option view filter', + context: { + createViewFilter: { + value: [ALL_OPTIONS[5].value], + }, + updateOptions: (options) => [fakeOptionUpdate(options[5])], + }, + }, + { + title: + 'should handle partial deletion of selected options in view filter', + context: { + updateOptions: (options) => options.slice(4), + }, + }, + { + title: + 'should handle reordering of options while maintaining view filter values', + context: { + createViewFilter: { + value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), + }, + updateOptions: (options) => [...options].reverse(), + }, + }, + { + title: + 'should handle no changes update of options while maintaining existing view filter values', + context: { + updateOptions: (options) => options, + }, + }, + { + title: + 'should handle adding new options while maintaining existing view filter', + context: { + fieldMetadata: { + options: ALL_OPTIONS.slice(0, 5), + type: fieldType, + }, + createViewFilter: { + value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), + }, + updateOptions: (options) => [ + ...options, + ...generateOptions(6).slice(5), + ], + }, + }, + ]; + + test.each(eachTestingContextFilter(testCases))( + '$title', + async ({ + context: { + expected, + createViewFilter = { + value: ALL_OPTIONS.map((option) => option.value), + }, + fieldMetadata = { options: ALL_OPTIONS, type: fieldType }, + updateOptions, + }, + }) => { + const { createOneField, createdView } = + await createObjectSelectFieldAndView(fieldMetadata); + + const { + data: { createCoreViewFilter: createdViewFilter }, + } = await createOneCoreViewFilter({ + input: { + viewId: createdView.id, + fieldMetadataId: createOneField.id, + operand: ViewFilterOperand.IS, + value: createViewFilter.value, + }, + expectToFail: false, + gqlFields: ` + id + `, + }); + + const optionsWithIds = createOneField.options; + + if (!isDefined(optionsWithIds)) { + throw new Error('optionsWithIds is not defined'); + } + const updatedOptions = updateOptions(optionsWithIds); + + await updateOneFieldMetadata({ + expectToFail: false, + input: { + idToUpdate: createOneField.id, + updatePayload: { + options: updatedOptions, + }, + }, + gqlFields: ` + id + options + `, + }); + + const updatedViewFilter = await findViewFilterWithRestApi( + createdViewFilter.id, + ); + + if (expected !== undefined) { + expect(updatedViewFilter).toBe(expected); + + return; + } + + if (!isDefined(updatedViewFilter)) { + throw new Error( + 'updatedViewFilter is not defined but should be at this point', + ); + } + + expect(updatedViewFilter.value).not.toBeNull(); + if (updatedViewFilter.value === null) { + throw new Error('Invariant parsedValue should not be null'); + } + expect(updatedOptions.map((option) => option.value)).toEqual( + expect.arrayContaining(updatedViewFilter.value as string[]), + ); + + expect({ + value: updatedViewFilter.value, + operand: updatedViewFilter.operand, + viewFilterGroupId: updatedViewFilter.viewFilterGroupId, + positionInViewFilterGroup: + updatedViewFilter.positionInViewFilterGroup, + subFieldName: updatedViewFilter.subFieldName, + }).toMatchSnapshot(); + }, + ); + + // Note these test exists only because we do not validate the view filter value on creation/update + // Should be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion + const failingTestCases: EachTestingContext<{ + createViewFilterValue: unknown; + }>[] = [ + { + title: + 'should throw error if view filter value is not a stringified JSON array', + context: { + createViewFilterValue: JSON.stringify( + 'not an array stringified json', + ), + }, + }, + ]; + + test.each(eachTestingContextFilter(failingTestCases))( + '$title', + async ({ context: { createViewFilterValue } }) => { + const { createOneField, createdView } = + await createObjectSelectFieldAndView({ + options: ALL_OPTIONS, + type: fieldType, + }); + + const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5'; + + await createOneCoreViewFilter({ + input: { + id: viewFilterId, + viewId: createdView.id, + fieldMetadataId: createOneField.id, + operand: ViewFilterOperand.IS, + value: createViewFilterValue as unknown as ViewFilterValue, + }, + expectToFail: false, + gqlFields: ` + id + `, + }); + + const optionsWithIds = createOneField.options; + + if (!isDefined(optionsWithIds)) { + throw new Error('optionsWithIds is not defined'); + } + const updatePayload = { + options: optionsWithIds.map((option) => fakeOptionUpdate(option)), + }; + const { errors, data } = await updateOneFieldMetadata({ + expectToFail: true, + input: { + idToUpdate: createOneField.id, + updatePayload, + }, + gqlFields: ` + id + options + `, + }); + + expect(data).toBeNull(); + expect(errors).toBeDefined(); + expect(errors).toMatchSnapshot(); + }, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts new file mode 100644 index 0000000000..15343fa785 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-view-groups-side-effect-v2.integration-spec.ts @@ -0,0 +1,452 @@ +import { faker } from '@faker-js/faker'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { createOneCoreViewGroup } from 'test/integration/metadata/suites/view-group/utils/create-one-core-view-group.util'; +import { findCoreViewGroups } from 'test/integration/metadata/suites/view-group/utils/find-core-view-groups.util'; +import { createOneCoreView } from 'test/integration/metadata/suites/view/utils/create-one-core-view.util'; +import { + FieldMetadataType, + type EnumFieldMetadataType, +} from 'twenty-shared/types'; + +import { + type FieldMetadataComplexOption, + type FieldMetadataDefaultOption, +} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; + +type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption; + +const generateOption = (index: number): Option => ({ + label: `Option ${index}`, + value: `OPTION_${index}`, + color: 'green', + position: index, +}); + +const generateOptions = (length: number) => + Array.from({ length }, (_value, index) => generateOption(index)); + +const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({ + ...option, + value: `${value}_UPDATED`, + label: `${label} updated`, +}); + +const testFieldMetadataTypes: EnumFieldMetadataType[] = [ + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, +]; + +describe('update-one-field-metadata-view-groups-side-effect-v2', () => { + let objectMetadataIdToDelete: string; + + const createObjectWithSelectFieldAndView = async ( + fieldType: EnumFieldMetadataType, + initialOptions: Option[], + ) => { + const singular = 'sideEffect'; + const plural = 'sideEffects'; + + const { + data: { createOneObject }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: getMockCreateObjectInput({ + labelSingular: singular, + labelPlural: plural, + nameSingular: singular, + namePlural: plural, + isLabelSyncedWithName: false, + }), + }); + + objectMetadataIdToDelete = createOneObject.id; + + const { + data: { createOneField }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + objectMetadataId: createOneObject.id, + type: fieldType, + name: 'statusField', + label: 'Status Field', + isLabelSyncedWithName: true, + options: initialOptions, + }, + gqlFields: 'id options', + }); + + const { + data: { createCoreView: view }, + } = await createOneCoreView({ + input: { + id: faker.string.uuid(), + icon: 'IconTable', + name: 'Test View', + objectMetadataId: createOneObject.id, + type: ViewType.TABLE, + }, + expectToFail: false, + gqlFields: 'id', + }); + + return { + objectMetadataId: createOneObject.id, + fieldMetadataId: createOneField.id, + fieldOptions: createOneField.options ?? [], + viewId: view.id, + }; + }; + + afterEach(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: objectMetadataIdToDelete, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: objectMetadataIdToDelete }, + }); + }); + + describe.each(testFieldMetadataTypes)('%s field type', (fieldType) => { + it('should delete all view groups when all enum field options are deleted', async () => { + const initialOptions = generateOptions(3); + const { fieldMetadataId, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + for (const [index, option] of initialOptions.entries()) { + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + } + + const { + data: { getCoreViewGroups: initialViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + expect( + initialViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId) + .length, + ).toBe(3); + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: [ + { + color: 'blue', + label: 'New option label', + position: 42, + value: 'NEW_OPTION_VALUE', + }, + ], + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + expect( + updatedViewGroups.filter((vg) => vg.fieldMetadataId === fieldMetadataId) + .length, + ).toBe(0); + }); + + it('should update view group when option value is updated', async () => { + const initialOptions = generateOptions(1); + const { fieldMetadataId, fieldOptions, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: initialOptions[0].value, + isVisible: true, + position: 0, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + + const updatedOptions = [fakeOptionUpdate(fieldOptions[0])]; + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: updatedOptions, + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + const viewGroupsForField = updatedViewGroups.filter( + (vg) => vg.fieldMetadataId === fieldMetadataId, + ); + + expect(viewGroupsForField.length).toBe(1); + expect(viewGroupsForField[0].fieldValue).toBe( + `${initialOptions[0].value}_UPDATED`, + ); + }); + + it('should update multiple view groups when multiple option values are updated', async () => { + const initialOptions = generateOptions(3); + const { fieldMetadataId, fieldOptions, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + for (const [index, option] of initialOptions.entries()) { + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + } + + const updatedOptions = fieldOptions.map((opt) => fakeOptionUpdate(opt)); + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: updatedOptions, + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + const viewGroupsForField = updatedViewGroups.filter( + (vg) => vg.fieldMetadataId === fieldMetadataId, + ); + + expect(viewGroupsForField.length).toBe(3); + const actualFieldValues = viewGroupsForField + .map((vg) => vg.fieldValue) + .sort(); + const expectedFieldValues = initialOptions + .map((opt) => `${opt.value}_UPDATED`) + .sort(); + + expect(actualFieldValues).toEqual(expectedFieldValues); + }); + + it('should delete specific view groups when their options are removed', async () => { + const initialOptions = generateOptions(5); + const { fieldMetadataId, fieldOptions, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + for (const [index, option] of initialOptions.entries()) { + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + } + + const updatedOptions = fieldOptions.slice(2); + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: updatedOptions, + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + const viewGroupsForField = updatedViewGroups.filter( + (vg) => vg.fieldMetadataId === fieldMetadataId, + ); + + expect(viewGroupsForField.length).toBe(3); + const remainingFieldValues = viewGroupsForField.map( + (vg) => vg.fieldValue, + ); + + expect(remainingFieldValues).not.toContain(initialOptions[0].value); + expect(remainingFieldValues).not.toContain(initialOptions[1].value); + expect(remainingFieldValues).toContain(initialOptions[2].value); + expect(remainingFieldValues).toContain(initialOptions[3].value); + expect(remainingFieldValues).toContain(initialOptions[4].value); + }); + + it('should preserve view groups when no options are changed', async () => { + const initialOptions = generateOptions(3); + const { fieldMetadataId, fieldOptions, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + for (const [index, option] of initialOptions.entries()) { + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + } + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: fieldOptions, + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + const viewGroupsForField = updatedViewGroups.filter( + (vg) => vg.fieldMetadataId === fieldMetadataId, + ); + + expect(viewGroupsForField.length).toBe(3); + const actualFieldValues = viewGroupsForField + .map((vg) => vg.fieldValue) + .sort(); + const expectedFieldValues = initialOptions.map((opt) => opt.value).sort(); + + expect(actualFieldValues).toEqual(expectedFieldValues); + }); + + it('should handle adding new options while maintaining existing view groups', async () => { + const initialOptions = generateOptions(3); + const { fieldMetadataId, fieldOptions, viewId } = + await createObjectWithSelectFieldAndView(fieldType, initialOptions); + + for (const [index, option] of initialOptions.entries()) { + await createOneCoreViewGroup({ + input: { + viewId, + fieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }, + expectToFail: false, + gqlFields: 'id fieldValue', + }); + } + + const newOptions = generateOptions(6).slice(3); + const updatedOptions = [...fieldOptions, ...newOptions]; + + await updateOneFieldMetadata({ + input: { + idToUpdate: fieldMetadataId, + updatePayload: { + options: updatedOptions, + }, + }, + gqlFields: 'id options', + expectToFail: false, + }); + + const { + data: { getCoreViewGroups: updatedViewGroups }, + } = await findCoreViewGroups({ + viewId, + gqlFields: 'id fieldValue fieldMetadataId', + expectToFail: false, + }); + + const viewGroupsForField = updatedViewGroups.filter( + (vg) => vg.fieldMetadataId === fieldMetadataId, + ); + + expect(viewGroupsForField.length).toBe(6); + const actualFieldValues = viewGroupsForField + .map((vg) => vg.fieldValue) + .sort(); + const expectedFieldValues = updatedOptions.map((opt) => opt.value).sort(); + + expect(actualFieldValues).toEqual(expectedFieldValues); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-view-operation-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/delete-core-view-query-factory.util.ts similarity index 74% rename from packages/twenty-server/test/integration/graphql/utils/delete-view-operation-factory.util.ts rename to packages/twenty-server/test/integration/metadata/suites/view/utils/delete-core-view-query-factory.util.ts index e88dbc035d..2fa08e8b7a 100644 --- a/packages/twenty-server/test/integration/graphql/utils/delete-view-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/delete-core-view-query-factory.util.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; -export const deleteViewOperationFactory = ({ viewId }: { viewId: string }) => ({ +export const deleteCoreViewQueryFactory = ({ viewId }: { viewId: string }) => ({ query: gql` mutation DeleteCoreView($id: String!) { deleteCoreView(id: $id) diff --git a/packages/twenty-server/test/integration/metadata/suites/view/utils/delete-one-core-view.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/delete-one-core-view.util.ts new file mode 100644 index 0000000000..859d14bf76 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/delete-one-core-view.util.ts @@ -0,0 +1,37 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { deleteCoreViewQueryFactory } from 'test/integration/metadata/suites/view/utils/delete-core-view-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const deleteOneCoreView = async ({ + viewId, + expectToFail, +}: { + viewId: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + deleteCoreView: boolean; +}> => { + const graphqlOperation = deleteCoreViewQueryFactory({ + viewId, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View deletion should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View deletion has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-view-operation-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-core-view-query-factory.util.ts similarity index 82% rename from packages/twenty-server/test/integration/graphql/utils/destroy-view-operation-factory.util.ts rename to packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-core-view-query-factory.util.ts index f29236f33a..6026418d8f 100644 --- a/packages/twenty-server/test/integration/graphql/utils/destroy-view-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-core-view-query-factory.util.ts @@ -1,6 +1,6 @@ import gql from 'graphql-tag'; -export const destroyViewOperationFactory = ({ +export const destroyCoreViewQueryFactory = ({ viewId, }: { viewId: string; diff --git a/packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-one-core-view.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-one-core-view.util.ts new file mode 100644 index 0000000000..b52239e04d --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/destroy-one-core-view.util.ts @@ -0,0 +1,37 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { destroyCoreViewQueryFactory } from 'test/integration/metadata/suites/view/utils/destroy-core-view-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const destroyOneCoreView = async ({ + viewId, + expectToFail, +}: { + viewId: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + destroyCoreView: boolean; +}> => { + const graphqlOperation = destroyCoreViewQueryFactory({ + viewId, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View destruction should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View destruction has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/find-view-operation-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/find-core-view-query-factory.util.ts similarity index 89% rename from packages/twenty-server/test/integration/graphql/utils/find-view-operation-factory.util.ts rename to packages/twenty-server/test/integration/metadata/suites/view/utils/find-core-view-query-factory.util.ts index 477503e335..2b56f530a3 100644 --- a/packages/twenty-server/test/integration/graphql/utils/find-view-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/find-core-view-query-factory.util.ts @@ -1,7 +1,7 @@ import gql from 'graphql-tag'; import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; -export const findViewOperationFactory = ({ +export const findCoreViewQueryFactory = ({ gqlFields = VIEW_GQL_FIELDS, viewId, }: { diff --git a/packages/twenty-server/test/integration/metadata/suites/view/utils/find-one-core-view.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/find-one-core-view.util.ts new file mode 100644 index 0000000000..9c761102ae --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/find-one-core-view.util.ts @@ -0,0 +1,42 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { findCoreViewQueryFactory } from 'test/integration/metadata/suites/view/utils/find-core-view-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto'; + +export const findOneCoreView = async ({ + viewId, + gqlFields, + expectToFail, +}: { + viewId: string; + gqlFields?: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + getCoreView: ViewDTO; +}> => { + const graphqlOperation = findCoreViewQueryFactory({ + viewId, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View search should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View search has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/update-view-operation-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/update-core-view-query-factory.util.ts similarity index 68% rename from packages/twenty-server/test/integration/graphql/utils/update-view-operation-factory.util.ts rename to packages/twenty-server/test/integration/metadata/suites/view/utils/update-core-view-query-factory.util.ts index 52d21d8074..7341115cc1 100644 --- a/packages/twenty-server/test/integration/graphql/utils/update-view-operation-factory.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/update-core-view-query-factory.util.ts @@ -1,14 +1,16 @@ import gql from 'graphql-tag'; import { VIEW_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants'; -export const updateViewOperationFactory = ({ +import { type UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input'; + +export const updateCoreViewQueryFactory = ({ gqlFields = VIEW_GQL_FIELDS, viewId, - data = {}, + input, }: { gqlFields?: string; viewId: string; - data?: object; + input: UpdateViewInput; }) => ({ query: gql` mutation UpdateCoreView($id: String!, $input: UpdateViewInput!) { @@ -19,6 +21,6 @@ export const updateViewOperationFactory = ({ `, variables: { id: viewId, - input: data, + input, }, }); diff --git a/packages/twenty-server/test/integration/metadata/suites/view/utils/update-one-core-view.util.ts b/packages/twenty-server/test/integration/metadata/suites/view/utils/update-one-core-view.util.ts new file mode 100644 index 0000000000..104b885926 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/view/utils/update-one-core-view.util.ts @@ -0,0 +1,46 @@ +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; +import { updateCoreViewQueryFactory } from 'test/integration/metadata/suites/view/utils/update-core-view-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +import { type ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto'; +import { type UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input'; + +export const updateOneCoreView = async ({ + viewId, + input, + gqlFields, + expectToFail, +}: { + viewId: string; + input: UpdateViewInput; + gqlFields?: string; + expectToFail?: boolean; +}): CommonResponseBody<{ + updateCoreView: ViewDTO; +}> => { + const graphqlOperation = updateCoreViewQueryFactory({ + viewId, + input, + gqlFields, + }); + + const response = await makeMetadataAPIRequest(graphqlOperation); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'View update should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'View update has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +};