From 61f1a883a0b4cc7b8fba21bd966e3b56befe3ed0 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 10 Sep 2025 16:55:30 +0200 Subject: [PATCH] Add workspace migration orchestrator + refactor builders + view/viewField action builders (#14383) --- .../exceptions/flat-entity.exception.ts | 14 + .../common/types/flat-entity-maps.type.ts | 6 + .../common/types/flat-entity.type.ts | 4 + ...ntity-to-flat-entity-maps-or-throw.util.ts | 36 ++ ...ity-from-flat-entity-maps-or-throw.util.ts | 40 +++ ...ntity-in-flat-entity-maps-or-throw.util.ts | 25 ++ .../view/cache/services/view-cache.module.ts | 12 + .../view/cache/services/view-cache.service.ts | 47 +++ .../view/entities/view-field.entity.ts | 9 +- .../core-modules/view/entities/view.entity.ts | 10 +- .../view/exceptions/flat-view.exception.ts | 13 + ...ew-field-properties-to-compare.constant.ts | 8 + ...flat-view-properies-to-compare.constant.ts | 14 + ...iew-entity-relation-properties.constant.ts | 11 + ...eld-entity-relation-properties.constant.ts | 7 + ...eld-validation-minimal-information.type.ts | 5 + .../failed-flat-view-field-validation.type.ts | 9 + ...iew-validation-minimal-information.type.ts | 5 + .../types/failed-flat-view-validation.type.ts | 9 + .../types/flat-view-field-maps.type.ts | 4 + ...t-view-field-properties-to-compare.type.ts | 4 + .../flat-view-field-validation-error.type.ts | 4 + .../flat-view/types/flat-view-field.type.ts | 18 + .../flat-view/types/flat-view-maps.type.ts | 4 + .../flat-view-properties-to-compare.type.ts | 4 + .../types/flat-view-validation-error.type.ts | 8 + .../view/flat-view/types/flat-view.type.ts | 26 ++ .../utils/compare-two-flat-view-field.util.ts | 59 ++++ .../utils/compare-two-flat-view.util.ts | 56 +++ ...ld-to-flat-view-field-with-default.util.ts | 27 ++ ...-view-to-flat-view-to-with-default.util.ts | 36 ++ .../from-view-entity-to-flat-view.util.ts | 20 ++ ...ew-field-entity-to-flat-view-field.util.ts | 21 ++ .../utils/generate-flat-view-maps.util.ts | 27 ++ .../view/services/view-field-v2.service.ts | 244 +++++++++++++ .../view/services/view-v2.service.ts | 219 ++++++++++++ .../engine/core-modules/view/view.module.ts | 10 + .../services/field-metadata.service-v2.ts | 6 +- .../object-metadata-v2.service.ts | 29 +- ...kspace-migration-orchestrator-exception.ts | 6 + ...ce-migration-build-orchestrator.service.ts | 189 ++++++++++ ...igration-validate-build-and-run-service.ts | 1 + .../types/flat-view-field.type.ts | 6 - .../types/flat-view.type.ts | 9 - .../workspace-migration-orchestrator.type.ts | 41 +++ .../workspace-migration-builder.spec.ts.snap | 333 ------------------ ...pace-migration-builder-object-test-case.ts | 5 +- .../workspace-migration-builder.spec.ts | 3 +- ...ration-v2-field-actions-builder.service.ts | 2 +- ...ation-v2-object-actions-builder.service.ts | 2 +- ...n-v2-view-field-actions-builder.service.ts | 178 ++++++++++ ...gration-v2-view-actions-builder.service.ts | 165 +++++++++ ...space-migration-v2-view-actions-builder.ts | 41 --- ...ace-entity-migration-builder-v2.service.ts | 102 ++++++ .../workspace-migration-builder-v2.service.ts | 24 +- ...view-field-migration-builder-v2.service.ts | 79 +++++ ...space-view-migration-builder-v2.service.ts | 78 ++++ .../validate-and-build-view-result.type.ts | 11 + .../workspace-migration-action-common-v2.ts | 4 +- ...workspace-migration-view-action-v2.type.ts | 30 +- ...ace-migration-view-field-action-v2.type.ts | 39 ++ .../get-workspace-migration-v2-view-action.ts | 21 ++ ...orkspace-migration-v2-view-field-action.ts | 21 ++ .../flat-view-field-validator.service.ts | 151 ++++++++ .../services/flat-view-validator.service.ts | 104 ++++++ ...ace-migration-builder-validators.module.ts | 10 + .../workspace-migration-builder-v2.module.ts | 17 +- ...reate-view-field-action-handler.service.ts | 37 ++ ...elete-view-field-action-handler.service.ts | 37 ++ ...pdate-view-field-action-handler.service.ts | 38 ++ .../create-view-action-handler.service.ts | 15 +- .../delete-view-action-handler.service.ts | 37 ++ .../update-view-action-handler.service.ts | 38 ++ ...migration-runner-action-handlers.module.ts | 11 + ...ion-action-on-flat-object-metadata-maps.ts | 5 + ...-to-partial-field-or-object-entity.util.ts | 8 +- .../workspace-migration-v2.module.ts | 15 +- .../interfaces/syncable-entity.interface.ts | 2 +- 78 files changed, 2557 insertions(+), 468 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/types/flat-entity-maps.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/types/flat-entity.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/exceptions/flat-view.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-maps.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-entity-to-flat-view.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/view/services/view-v2.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service.ts delete mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-view-result.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service.ts diff --git a/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts b/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts new file mode 100644 index 0000000000..f93d6b95de --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/exceptions/flat-entity.exception.ts @@ -0,0 +1,14 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class FlatEntityException extends CustomException { + code: FlatEntityExceptionCode; + + constructor(message: string, code: FlatEntityExceptionCode) { + super(message, code); + } +} + +export enum FlatEntityExceptionCode { + ENTITY_ALREADY_EXISTS = 'ENTITY_ALREADY_EXISTS', + ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/core-modules/common/types/flat-entity-maps.type.ts b/packages/twenty-server/src/engine/core-modules/common/types/flat-entity-maps.type.ts new file mode 100644 index 0000000000..91c6907522 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/types/flat-entity-maps.type.ts @@ -0,0 +1,6 @@ +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; + +export type FlatEntityMaps = { + byId: Partial>; + idByUniversalIdentifier: Partial>; +}; diff --git a/packages/twenty-server/src/engine/core-modules/common/types/flat-entity.type.ts b/packages/twenty-server/src/engine/core-modules/common/types/flat-entity.type.ts new file mode 100644 index 0000000000..72359b083b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/types/flat-entity.type.ts @@ -0,0 +1,4 @@ +export interface FlatEntity { + id: string; + universalIdentifier: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts new file mode 100644 index 0000000000..707918202f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util.ts @@ -0,0 +1,36 @@ +import { isDefined } from 'class-validator'; + +import { + FlatEntityException, + FlatEntityExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity.exception'; +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; + +type AddFlatEntityToFlatEntityMapsOrThrowArgs = { + flatEntity: T; + flatEntityMaps: FlatEntityMaps; +}; + +export const addFlatEntityToFlatEntityMapsOrThrow = ({ + flatEntity, + flatEntityMaps, +}: AddFlatEntityToFlatEntityMapsOrThrowArgs): FlatEntityMaps => { + if (isDefined(flatEntityMaps.byId[flatEntity.id])) { + throw new FlatEntityException( + 'addFlatEntityToFlatEntityMapsOrThrow: flat entity to add already exists', + FlatEntityExceptionCode.ENTITY_ALREADY_EXISTS, + ); + } + + return { + byId: { + ...flatEntityMaps.byId, + [flatEntity.id]: flatEntity, + }, + idByUniversalIdentifier: { + ...flatEntityMaps.idByUniversalIdentifier, + [flatEntity.universalIdentifier]: flatEntity.id, + }, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts new file mode 100644 index 0000000000..1b00662ac1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util.ts @@ -0,0 +1,40 @@ +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; +import { + FlatEntityException, + FlatEntityExceptionCode, +} from 'src/engine/core-modules/common/exceptions/flat-entity.exception'; + +export type DeleteFlatEntityFromFlatEntityMapsOrThrowArgs< + T extends FlatEntity, +> = { + entityToDeleteId: string; + flatEntityMaps: FlatEntityMaps; +}; + +export const deleteFlatEntityFromFlatEntityMapsOrThrow = < + T extends FlatEntity, +>({ + flatEntityMaps, + entityToDeleteId, +}: DeleteFlatEntityFromFlatEntityMapsOrThrowArgs): FlatEntityMaps => { + if (!isDefined(flatEntityMaps.byId[entityToDeleteId])) { + throw new FlatEntityException( + 'deleteFlatEntityFromFlatEntityMapsOrThrow: entity to delete not found', + FlatEntityExceptionCode.ENTITY_NOT_FOUND, + ); + } + + const updatedIdByUniversalIdentifierEntries = Object.entries( + flatEntityMaps.idByUniversalIdentifier, + ).filter(([_universalIdentifier, id]) => id !== entityToDeleteId); + + return { + byId: removePropertiesFromRecord(flatEntityMaps.byId, [entityToDeleteId]), + idByUniversalIdentifier: Object.fromEntries( + updatedIdByUniversalIdentifierEntries, + ), + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util.ts b/packages/twenty-server/src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util.ts new file mode 100644 index 0000000000..8d3ccc379c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util.ts @@ -0,0 +1,25 @@ +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; + +export type ReplaceFlatEntityInFlatEntityMapsOrThrowArgs = + { + flatEntity: T; + flatEntityMaps: FlatEntityMaps; + }; + +export const replaceFlatEntityInFlatEntityMapsOrThrow = ({ + flatEntity, + flatEntityMaps, +}: ReplaceFlatEntityInFlatEntityMapsOrThrowArgs): FlatEntityMaps => { + const flatEntityMapsToReplace = deleteFlatEntityFromFlatEntityMapsOrThrow({ + flatEntityMaps, + entityToDeleteId: flatEntity.id, + }); + + return addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity, + flatEntityMaps: flatEntityMapsToReplace, + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.module.ts b/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.module.ts new file mode 100644 index 0000000000..0947e3556d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service'; +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([ViewEntity])], + providers: [ViewCacheService], + exports: [ViewCacheService], +}) +export class ViewCacheModule {} diff --git a/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.service.ts b/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.service.ts new file mode 100644 index 0000000000..1e6467dca9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/cache/services/view-cache.service.ts @@ -0,0 +1,47 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { generateFlatViewMaps } from 'src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util'; + +type GetExistingOrRecomputeFlatViewMapsResult = { + flatViewMaps: FlatViewMaps; +}; + +@Injectable() +export class ViewCacheService { + logger = new Logger(ViewCacheService.name); + + constructor( + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, + ) {} + + async getExistingOrRecomputeFlatViewMaps({ + workspaceId, + }: { + workspaceId: string; + }): Promise { + // TODO: get from cache later + const existingViews = await this.viewRepository.find({ + where: { + workspaceId, + }, + relations: ['viewFields'], + select: { + viewFields: { + id: true, + }, + }, + }); + + const existingFlatViewMaps = generateFlatViewMaps(existingViews); + + return { + flatViewMaps: existingFlatViewMaps, + }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts index 91a6f26dc0..5f5af9117c 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts @@ -31,7 +31,10 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ where: '"deletedAt" IS NULL', }, ) -export class ViewFieldEntity extends SyncableEntity { +export class ViewFieldEntity + extends SyncableEntity + implements Required +{ @PrimaryGeneratedColumn('uuid') id: string; @@ -59,7 +62,7 @@ export class ViewFieldEntity extends SyncableEntity { nullable: true, default: null, }) - aggregateOperation?: AggregateOperations | null; + aggregateOperation: AggregateOperations | null; @Column({ nullable: false, type: 'uuid' }) viewId: string; @@ -74,7 +77,7 @@ export class ViewFieldEntity extends SyncableEntity { updatedAt: Date; @DeleteDateColumn({ type: 'timestamptz' }) - deletedAt?: Date | null; + deletedAt: Date | null; @ManyToOne(() => Workspace, { onDelete: 'CASCADE', diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts index 07850cdbcf..60e4181be9 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view.entity.ts @@ -31,7 +31,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat 'workspaceId', 'objectMetadataId', ]) -export class ViewEntity extends SyncableEntity { +export class ViewEntity extends SyncableEntity implements Required { @PrimaryGeneratedColumn('uuid') id: string; @@ -89,10 +89,10 @@ export class ViewEntity extends SyncableEntity { nullable: true, default: null, }) - kanbanAggregateOperation?: AggregateOperations | null; + kanbanAggregateOperation: AggregateOperations | null; @Column({ nullable: true, type: 'uuid' }) - kanbanAggregateOperationFieldMetadataId?: string | null; + kanbanAggregateOperationFieldMetadataId: string | null; @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @@ -104,10 +104,10 @@ export class ViewEntity extends SyncableEntity { updatedAt: Date; @DeleteDateColumn({ type: 'timestamptz' }) - deletedAt?: Date | null; + deletedAt: Date | null; @Column({ nullable: true, type: 'text', default: null }) - anyFieldFilterValue?: string | null; + anyFieldFilterValue: string | null; @ManyToOne(() => Workspace, { onDelete: 'CASCADE', diff --git a/packages/twenty-server/src/engine/core-modules/view/exceptions/flat-view.exception.ts b/packages/twenty-server/src/engine/core-modules/view/exceptions/flat-view.exception.ts new file mode 100644 index 0000000000..e28c13e61d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/exceptions/flat-view.exception.ts @@ -0,0 +1,13 @@ +import { + appendCommonExceptionCode, + CustomException, +} from 'src/utils/custom-exception'; + +export class FlatViewException extends CustomException< + keyof typeof FlatViewExceptionCode +> {} + +export const FlatViewExceptionCode = appendCommonExceptionCode({ + VIEW_NOT_FOUND: 'VIEW_NOT_FOUND', + VIEW_ALREADY_EXISTS: 'VIEW_ALREADY_EXISTS', +} as const); diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant.ts new file mode 100644 index 0000000000..69f34bdb3d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant.ts @@ -0,0 +1,8 @@ +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE = [ + 'isVisible', + 'size', + 'position', + 'aggregateOperation', +] as const satisfies (keyof FlatViewField)[]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant.ts new file mode 100644 index 0000000000..176367a564 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant.ts @@ -0,0 +1,14 @@ +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export const FLAT_VIEW_PROPERTIES_TO_COMPARE = [ + 'name', + 'type', + 'key', + 'isCompact', + 'openRecordIn', + 'kanbanAggregateOperation', + 'kanbanAggregateOperationFieldMetadataId', + 'position', + 'anyFieldFilterValue', + 'icon', +] as const satisfies (keyof FlatView)[]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant.ts new file mode 100644 index 0000000000..b1f413fc07 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant.ts @@ -0,0 +1,11 @@ +import { type ViewEntityRelationProperties } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export const VIEW_ENTITY_RELATION_PROPERTIES = [ + 'objectMetadata', + 'viewFields', + 'viewFilters', + 'viewFilterGroups', + 'viewGroups', + 'viewSorts', + 'workspace', +] as const satisfies ViewEntityRelationProperties[]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant.ts new file mode 100644 index 0000000000..7313786c34 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant.ts @@ -0,0 +1,7 @@ +import { type ViewFieldEntityRelationProperties } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const VIEW_FIELD_ENTITY_RELATION_PROPERTIES = [ + 'fieldMetadata', + 'view', + 'workspace', +] as const satisfies ViewFieldEntityRelationProperties[]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type.ts new file mode 100644 index 0000000000..5f0e4f5c3e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type.ts @@ -0,0 +1,5 @@ +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export type FailedFlatViewFieldValidationMinimalInformation = Partial< + Pick +>; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type.ts new file mode 100644 index 0000000000..6be44ee8d8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type.ts @@ -0,0 +1,9 @@ +import { type FailedFlatViewFieldValidationMinimalInformation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation-minimal-information.type'; +import { type FlatViewFieldValidationError } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type'; +import { type WorkspaceMigrationViewFieldActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; + +export type FailedFlatViewFieldValidation = { + type: WorkspaceMigrationViewFieldActionTypeV2; + viewFieldLevelErrors: FlatViewFieldValidationError[]; + failedViewFieldValidationMinimalInformation: Partial; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type.ts new file mode 100644 index 0000000000..6908af78ad --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type.ts @@ -0,0 +1,5 @@ +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export type FailedFlatViewValidationMinimalInformation = Partial< + Pick +>; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type.ts new file mode 100644 index 0000000000..35d8971297 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type.ts @@ -0,0 +1,9 @@ +import { type FailedFlatViewValidationMinimalInformation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation-minimal-information.type'; +import { type FlatViewValidationError } from 'src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type'; +import { type WorkspaceMigrationViewActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; + +export type FailedFlatViewValidation = { + type: WorkspaceMigrationViewActionTypeV2; + viewLevelErrors: FlatViewValidationError[]; + failedViewValidationMinimalInformation: Partial; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type.ts new file mode 100644 index 0000000000..26f9acaf05 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export type FlatViewFieldMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type.ts new file mode 100644 index 0000000000..9218aec140 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant'; + +export type FlatViewFieldPropertiesToCompare = + (typeof FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE)[number]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type.ts new file mode 100644 index 0000000000..18b3311188 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field-validation-error.type.ts @@ -0,0 +1,4 @@ +export type FlatViewFieldValidationError = { + message: string; + code: string; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field.type.ts new file mode 100644 index 0000000000..054b827fd6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-field.type.ts @@ -0,0 +1,18 @@ +import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type'; + +export type ViewFieldEntityRelationProperties = + ExtractRecordTypeOrmRelationProperties< + ViewFieldEntity, + FieldMetadataEntity | ViewEntity | Workspace + >; + +export type FlatViewField = Omit< + ViewFieldEntity, + ViewFieldEntityRelationProperties +> & { + universalIdentifier: string; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-maps.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-maps.type.ts new file mode 100644 index 0000000000..6862648b79 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-maps.type.ts @@ -0,0 +1,4 @@ +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export type FlatViewMaps = FlatEntityMaps; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type.ts new file mode 100644 index 0000000000..2c8a4c12d1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type.ts @@ -0,0 +1,4 @@ +import { type FLAT_VIEW_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant'; + +export type FlatViewPropertiesToCompare = + (typeof FLAT_VIEW_PROPERTIES_TO_COMPARE)[number]; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type.ts new file mode 100644 index 0000000000..aa0869d643 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view-validation-error.type.ts @@ -0,0 +1,8 @@ +import { type ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception'; + +export type FlatViewValidationError = { + code: ViewExceptionCode; + message: string; + userFriendlyMessage?: string; + value?: unknown; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view.type.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view.type.ts new file mode 100644 index 0000000000..cb30731c93 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/types/flat-view.type.ts @@ -0,0 +1,26 @@ +import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { type ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity'; +import { type ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity'; +import { type ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity'; +import { type ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity'; +import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type'; + +export type ViewEntityRelationProperties = + ExtractRecordTypeOrmRelationProperties< + ViewEntity, + | ObjectMetadataEntity + | ViewFieldEntity + | ViewSortEntity + | ViewFilterEntity + | ViewGroupEntity + | ViewFilterGroupEntity + | Workspace + >; + +export type FlatView = Omit & { + universalIdentifier: string; + viewFieldIds: string[]; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util.ts new file mode 100644 index 0000000000..e09d9418f9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util.ts @@ -0,0 +1,59 @@ +import diff from 'microdiff'; +import { type FromTo } from 'twenty-shared/types'; + +import { FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-field-properties-to-compare.constant'; +import { type FlatViewFieldPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { type UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +type GetWorkspaceMigrationUpdateViewFieldActionArgs = FromTo< + FlatViewField, + 'FlatViewField' +>; + +export const compareTwoFlatViewField = ({ + fromFlatViewField, + toFlatViewField, +}: GetWorkspaceMigrationUpdateViewFieldActionArgs) => { + const transformMetadataForComparisonParameters = { + shouldIgnoreProperty: (property: string) => + !FLAT_VIEW_FIELD_PROPERTIES_TO_COMPARE.includes( + property as FlatViewFieldPropertiesToCompare, + ), + propertiesToStringify: [], + }; + const fromCompare = transformMetadataForComparison( + fromFlatViewField, + transformMetadataForComparisonParameters, + ); + const toCompare = transformMetadataForComparison( + toFlatViewField, + transformMetadataForComparisonParameters, + ); + + const flatViewFieldDifferences = diff(fromCompare, toCompare); + + return flatViewFieldDifferences.flatMap< + UpdateViewFieldAction['updates'][number] + >((difference) => { + switch (difference.type) { + case 'CHANGE': { + const { oldValue, path, value } = difference; + const property = path[0] as FlatViewFieldPropertiesToCompare; + + return { + from: oldValue, + to: value, + property, + }; + } + case 'CREATE': + case 'REMOVE': + default: { + // Should never occurs, we should only provide null never undefined and so on + return []; + } + } + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util.ts new file mode 100644 index 0000000000..5583ee19be --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util.ts @@ -0,0 +1,56 @@ +import diff from 'microdiff'; +import { type FromTo } from 'twenty-shared/types'; + +import { FLAT_VIEW_PROPERTIES_TO_COMPARE } from 'src/engine/core-modules/view/flat-view/constants/flat-view-properies-to-compare.constant'; +import { type FlatViewPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; +import { type UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +type GetWorkspaceMigrationUpdateViewActionArgs = FromTo; + +export const compareTwoFlatView = ({ + fromFlatView, + toFlatView, +}: GetWorkspaceMigrationUpdateViewActionArgs) => { + const transformMetadataForComparisonParameters = { + shouldIgnoreProperty: (property: string) => + !FLAT_VIEW_PROPERTIES_TO_COMPARE.includes( + property as FlatViewPropertiesToCompare, + ), + propertiesToStringify: [], + }; + const fromCompare = transformMetadataForComparison( + fromFlatView, + transformMetadataForComparisonParameters, + ); + const toCompare = transformMetadataForComparison( + toFlatView, + transformMetadataForComparisonParameters, + ); + + const flatViewDifferences = diff(fromCompare, toCompare); + + return flatViewDifferences.flatMap( + (difference) => { + switch (difference.type) { + case 'CHANGE': { + const { oldValue, path, value } = difference; + const property = path[0] as FlatViewPropertiesToCompare; + + return { + from: oldValue, + to: value, + property, + }; + } + case 'CREATE': + case 'REMOVE': + default: { + // Should never occurs, we should only provide null never undefined and so on + return []; + } + } + }, + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts new file mode 100644 index 0000000000..b97ec21d0f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util.ts @@ -0,0 +1,27 @@ +import { v4 } from 'uuid'; + +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const fromPartialFlatViewFieldToFlatViewFieldWithDefault = ( + partialFlatViewField: Partial, +): FlatViewField => { + const createdAt = new Date(); + const viewFieldId = partialFlatViewField.id ?? v4(); + + return { + ...partialFlatViewField, + id: viewFieldId, + fieldMetadataId: partialFlatViewField.fieldMetadataId ?? '', + isVisible: partialFlatViewField.isVisible ?? true, + size: partialFlatViewField.size ?? 0, + position: partialFlatViewField.position ?? 0, + aggregateOperation: partialFlatViewField.aggregateOperation ?? null, + viewId: partialFlatViewField.viewId ?? '', + workspaceId: partialFlatViewField.workspaceId ?? '', + createdAt: createdAt, + updatedAt: createdAt, + deletedAt: null, + universalIdentifier: + partialFlatViewField.universalIdentifier ?? viewFieldId, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts new file mode 100644 index 0000000000..a98e7658bd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util.ts @@ -0,0 +1,36 @@ +import { v4 } from 'uuid'; + +import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; +import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export const fromPartialFlatViewToFlatViewWithDefault = ( + partialFlatView: Partial, +): FlatView => { + const createdAt = new Date(); + const viewId = partialFlatView.id ?? v4(); + + return { + ...partialFlatView, + id: viewId, + name: partialFlatView.name ?? '', + objectMetadataId: partialFlatView.objectMetadataId ?? '', + type: partialFlatView.type ?? ViewType.TABLE, + key: partialFlatView.key ?? null, + icon: partialFlatView.icon ?? '', + position: partialFlatView.position ?? 0, + isCompact: partialFlatView.isCompact ?? false, + isCustom: partialFlatView.isCustom ?? false, + openRecordIn: partialFlatView.openRecordIn ?? ViewOpenRecordIn.SIDE_PANEL, + kanbanAggregateOperation: partialFlatView.kanbanAggregateOperation ?? null, + kanbanAggregateOperationFieldMetadataId: + partialFlatView.kanbanAggregateOperationFieldMetadataId ?? null, + workspaceId: partialFlatView.workspaceId ?? '', + createdAt: createdAt, + updatedAt: createdAt, + deletedAt: null, + anyFieldFilterValue: partialFlatView.anyFieldFilterValue ?? null, + universalIdentifier: partialFlatView.universalIdentifier ?? viewId, + viewFieldIds: partialFlatView.viewFieldIds ?? [], + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-entity-to-flat-view.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-entity-to-flat-view.util.ts new file mode 100644 index 0000000000..24b72131d8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-entity-to-flat-view.util.ts @@ -0,0 +1,20 @@ +import { removePropertiesFromRecord } from 'twenty-shared/utils'; + +import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { VIEW_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => { + const viewEntityWithoutRelations = removePropertiesFromRecord( + viewEntity, + VIEW_ENTITY_RELATION_PROPERTIES, + ); + + return { + ...viewEntityWithoutRelations, + universalIdentifier: + viewEntityWithoutRelations.universalIdentifier ?? + viewEntityWithoutRelations.id, + viewFieldIds: viewEntity.viewFields?.map((viewField) => viewField.id) ?? [], + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util.ts new file mode 100644 index 0000000000..ff755014aa --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util.ts @@ -0,0 +1,21 @@ +import { removePropertiesFromRecord } from 'twenty-shared/utils'; + +import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export const fromViewFieldEntityToFlatViewField = ( + viewFieldEntity: ViewFieldEntity, +): FlatViewField => { + const viewFieldEntityWithoutRelations = removePropertiesFromRecord( + viewFieldEntity, + VIEW_FIELD_ENTITY_RELATION_PROPERTIES, + ); + + return { + ...viewFieldEntityWithoutRelations, + universalIdentifier: + viewFieldEntityWithoutRelations.universalIdentifier ?? + viewFieldEntityWithoutRelations.id, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util.ts b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util.ts new file mode 100644 index 0000000000..32dd80ed4d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/flat-view/utils/generate-flat-view-maps.util.ts @@ -0,0 +1,27 @@ +import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export const generateFlatViewMaps = ( + viewCollection: ViewEntity[], +): FlatViewMaps => { + const flatViewMaps: FlatViewMaps = { + byId: {}, + idByUniversalIdentifier: {}, + }; + + for (const view of viewCollection) { + const processedFlatView: FlatView = { + ...view, + viewFieldIds: view.viewFields?.map((viewField) => viewField.id) ?? [], + universalIdentifier: view.universalIdentifier ?? view.id, // TODO: should not fallback to id, to remove once we have universalIdentifier required + }; + + flatViewMaps.byId[view.id] = processedFlatView; + flatViewMaps.idByUniversalIdentifier[ + processedFlatView.universalIdentifier + ] = view.id; + } + + return flatViewMaps; +}; diff --git a/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts new file mode 100644 index 0000000000..d76b21b8ff --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/services/view-field-v2.service.ts @@ -0,0 +1,244 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { Equal, Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { + ViewFieldException, + ViewFieldExceptionCode, + ViewFieldExceptionMessageKey, + generateViewFieldExceptionMessage, + generateViewFieldUserFriendlyExceptionMessage, +} from 'src/engine/core-modules/view/exceptions/view-field.exception'; +import { VIEW_FIELD_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-field-entity-relation-properties.constant'; +import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { fromPartialFlatViewFieldToFlatViewFieldWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-field-to-flat-view-field-with-default.util'; +import { fromViewFieldEntityToFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/from-view-field-entity-to-flat-view-field.util'; +import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception'; +import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service'; + +@Injectable() +export class ViewFieldV2Service { + constructor( + @InjectRepository(ViewFieldEntity) + private readonly viewFieldRepository: Repository, + private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService, + ) {} + + // TODO: move to cache service + private async getExistingFlatViewFieldMaps( + workspaceId: string, + ): Promise { + const existingViewFields = await this.viewFieldRepository.find({ + where: { workspaceId }, + }); + + const flatViewFieldMaps: FlatViewFieldMaps = { + byId: {}, + idByUniversalIdentifier: {}, + }; + + for (const viewFieldEntity of existingViewFields) { + const flatViewField = fromViewFieldEntityToFlatViewField(viewFieldEntity); + + flatViewFieldMaps.byId[flatViewField.id] = flatViewField; + flatViewFieldMaps.idByUniversalIdentifier[ + flatViewField.universalIdentifier + ] = flatViewField.id; + } + + return flatViewFieldMaps; + } + + async createOne( + viewFieldData: Partial, + ): Promise { + if (!isDefined(viewFieldData.workspaceId)) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED, + ), + ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA, + { + userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage( + ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED, + ), + }, + ); + } + + const existingFlatViewFieldMaps = await this.getExistingFlatViewFieldMaps( + viewFieldData.workspaceId, + ); + + const flatViewFieldFromCreateInput = + fromPartialFlatViewFieldToFlatViewFieldWithDefault({ + ...viewFieldData, + universalIdentifier: viewFieldData.universalIdentifier ?? v4(), + }); + + const toFlatViewFieldMaps: FlatViewFieldMaps = + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatViewFieldFromCreateInput, + flatEntityMaps: existingFlatViewFieldMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + viewField: { + fromFlatViewFieldMaps: existingFlatViewFieldMaps, + toFlatViewFieldMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: false, + }, + workspaceId: viewFieldData.workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while creating view field', + ); + } + + const [createdViewField] = await this.viewFieldRepository.find({ + where: { + id: flatViewFieldFromCreateInput.id, + }, + }); + + return createdViewField; + } + + async updateOne( + id: string, + workspaceId: string, + updateData: Partial, + ): Promise { + const existingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMaps(workspaceId); + + const existingViewField = existingFlatViewFieldMaps.byId[id]; + + if (!isDefined(existingViewField)) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + id, + ), + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + ); + } + + const existingViewFieldToUpdate = removePropertiesFromRecord( + { + ...existingViewField, + ...updateData, + }, + VIEW_FIELD_ENTITY_RELATION_PROPERTIES, + ); + + const flatViewFieldFromUpdateInput = + fromPartialFlatViewFieldToFlatViewFieldWithDefault({ + ...existingViewFieldToUpdate, + universalIdentifier: + existingViewFieldToUpdate.universalIdentifier ?? '', + }); + + const toFlatViewFieldMaps: FlatViewFieldMaps = + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatViewFieldFromUpdateInput, + flatEntityMaps: existingFlatViewFieldMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + viewField: { + fromFlatViewFieldMaps: existingFlatViewFieldMaps, + toFlatViewFieldMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: false, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while updating view field', + ); + } + + const [updatedViewField] = await this.viewFieldRepository.find({ + where: { + id: Equal(id), + }, + }); + + return updatedViewField; + } + + async deleteOne(id: string, workspaceId: string): Promise { + const existingFlatViewFieldMaps = + await this.getExistingFlatViewFieldMaps(workspaceId); + + const existingViewFieldToDelete = existingFlatViewFieldMaps.byId[id]; + + if (!isDefined(existingViewFieldToDelete)) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + id, + ), + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + ); + } + + const toFlatViewFieldMaps: FlatViewFieldMaps = + deleteFlatEntityFromFlatEntityMapsOrThrow({ + flatEntityMaps: existingFlatViewFieldMaps, + entityToDeleteId: existingViewFieldToDelete.id, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + viewField: { + fromFlatViewFieldMaps: existingFlatViewFieldMaps, + toFlatViewFieldMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: true, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while deleting view field', + ); + } + + return true; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/view/services/view-v2.service.ts b/packages/twenty-server/src/engine/core-modules/view/services/view-v2.service.ts new file mode 100644 index 0000000000..e2d97f630d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/view/services/view-v2.service.ts @@ -0,0 +1,219 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils'; +import { Equal, Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service'; +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { + ViewException, + ViewExceptionCode, + ViewExceptionMessageKey, + generateViewExceptionMessage, + generateViewUserFriendlyExceptionMessage, +} from 'src/engine/core-modules/view/exceptions/view.exception'; +import { VIEW_ENTITY_RELATION_PROPERTIES } from 'src/engine/core-modules/view/flat-view/constants/view-entity-relation-properties.constant'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { fromPartialFlatViewToFlatViewWithDefault } from 'src/engine/core-modules/view/flat-view/utils/from-partial-flat-view-to-flat-view-to-with-default.util'; +import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception'; +import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service'; + +@Injectable() +export class ViewV2Service { + constructor( + @InjectRepository(ViewEntity) + private readonly viewRepository: Repository, + private readonly workspaceMigrationOrchestratorService: WorkspaceMigrationBuildOrchestratorService, + private readonly viewCacheService: ViewCacheService, + ) {} + + async createOne(viewData: Partial): Promise { + if (!isDefined(viewData.workspaceId)) { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED, + ), + ViewExceptionCode.INVALID_VIEW_DATA, + { + userFriendlyMessage: generateViewUserFriendlyExceptionMessage( + ViewExceptionMessageKey.WORKSPACE_ID_REQUIRED, + ), + }, + ); + } + + const { flatViewMaps: existingFlatViewMaps } = + await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({ + workspaceId: viewData.workspaceId, + }); + + const flatViewFromCreateInput = fromPartialFlatViewToFlatViewWithDefault({ + ...viewData, + universalIdentifier: viewData.universalIdentifier ?? v4(), + }); + + const toFlatViewMaps: FlatViewMaps = addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatViewFromCreateInput, + flatEntityMaps: existingFlatViewMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + view: { + fromFlatViewMaps: existingFlatViewMaps, + toFlatViewMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: false, + }, + workspaceId: viewData.workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while creating view', + ); + } + + const [createdView] = await this.viewRepository.find({ + where: { + id: flatViewFromCreateInput.id, + }, + }); + + return createdView; + } + + async updateOne( + id: string, + workspaceId: string, + updateData: Partial, + ): Promise { + const { flatViewMaps: existingFlatViewMaps } = + await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({ + workspaceId, + }); + + const existingView = existingFlatViewMaps.byId[id]; + + if (!isDefined(existingView)) { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.VIEW_NOT_FOUND, + id, + ), + ViewExceptionCode.VIEW_NOT_FOUND, + ); + } + + const existingViewToUpdate = removePropertiesFromRecord( + { + ...existingView, + ...updateData, + }, + VIEW_ENTITY_RELATION_PROPERTIES, + ); + + const flatViewFromUpdateInput = fromPartialFlatViewToFlatViewWithDefault({ + ...existingViewToUpdate, + universalIdentifier: existingViewToUpdate.universalIdentifier ?? '', + }); + + const toFlatViewMaps: FlatViewMaps = + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: flatViewFromUpdateInput, + flatEntityMaps: existingFlatViewMaps, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + view: { + fromFlatViewMaps: existingFlatViewMaps, + toFlatViewMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: false, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while updating view', + ); + } + + const [updatedView] = await this.viewRepository.find({ + where: { + id: Equal(id), + }, + }); + + return updatedView; + } + + async deleteOne(id: string, workspaceId: string): Promise { + const { flatViewMaps: existingFlatViewMaps } = + await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({ + workspaceId, + }); + + const existingViewToDelete = existingFlatViewMaps.byId[id]; + + if (!isDefined(existingViewToDelete)) { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.VIEW_NOT_FOUND, + id, + ), + ViewExceptionCode.VIEW_NOT_FOUND, + ); + } + + const toFlatViewMaps: FlatViewMaps = + deleteFlatEntityFromFlatEntityMapsOrThrow({ + flatEntityMaps: existingFlatViewMaps, + entityToDeleteId: existingViewToDelete.id, + }); + + const validateAndBuildResult = + await this.workspaceMigrationOrchestratorService.buildWorkspaceMigrations( + { + entityMaps: { + view: { + fromFlatViewMaps: existingFlatViewMaps, + toFlatViewMaps, + }, + }, + buildOptions: { + isSystemBuild: false, + inferDeletionFromMissingEntities: true, + }, + workspaceId, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationOrchestratorException( + 'Multiple validation errors occurred while deleting view', + ); + } + + return true; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/view/view.module.ts b/packages/twenty-server/src/engine/core-modules/view/view.module.ts index fd113aae6f..f656005029 100644 --- a/packages/twenty-server/src/engine/core-modules/view/view.module.ts +++ b/packages/twenty-server/src/engine/core-modules/view/view.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module'; +import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module'; import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller'; import { ViewFilterGroupController } from 'src/engine/core-modules/view/controllers/view-filter-group.controller'; import { ViewFilterController } from 'src/engine/core-modules/view/controllers/view-filter.controller'; @@ -20,14 +21,17 @@ import { ViewFilterResolver } from 'src/engine/core-modules/view/resolvers/view- import { ViewGroupResolver } from 'src/engine/core-modules/view/resolvers/view-group.resolver'; import { ViewSortResolver } from 'src/engine/core-modules/view/resolvers/view-sort.resolver'; import { ViewResolver } from 'src/engine/core-modules/view/resolvers/view.resolver'; +import { ViewFieldV2Service } from 'src/engine/core-modules/view/services/view-field-v2.service'; import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service'; import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service'; import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service'; import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service'; +import { ViewV2Service } from 'src/engine/core-modules/view/services/view-v2.service'; import { ViewService } from 'src/engine/core-modules/view/services/view.service'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; @Module({ imports: [ @@ -42,6 +46,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ I18nModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, + WorkspaceMigrationV2Module, + ViewCacheModule, ], controllers: [ ViewController, @@ -64,6 +70,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ViewFilterGroupResolver, ViewGroupResolver, ViewSortResolver, + ViewV2Service, + ViewFieldV2Service, ], exports: [ ViewService, @@ -72,6 +80,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ ViewFilterGroupService, ViewGroupService, ViewSortService, + ViewV2Service, + ViewFieldV2Service, ], }) export class CoreViewModule {} 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 4b5a5f82d0..f8a3caed7e 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 @@ -105,7 +105,7 @@ export class FieldMetadataServiceV2 { { buildOptions: { isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: true, + inferDeletionFromMissingEntities: true, }, fromFlatObjectMetadataMaps: flatObjectMetadataMapsWithImpactedObject, toFlatObjectMetadataMaps, @@ -181,7 +181,7 @@ export class FieldMetadataServiceV2 { toFlatObjectMetadataMaps, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -270,7 +270,7 @@ export class FieldMetadataServiceV2 { toFlatObjectMetadataMaps, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + inferDeletionFromMissingEntities: false, }, workspaceId, }, 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 120486a72d..43565d7865 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 @@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; -import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; +import { ViewV2Service } from 'src/engine/core-modules/view/services/view-v2.service'; import { addFlatFieldMetadataInFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-field-metadata-in-flat-object-metadata-maps-or-throw.util'; import { addFlatObjectMetadataToFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/add-flat-object-metadata-to-flat-object-metadata-maps-or-throw.util'; import { deleteFieldFromFlatObjectMetadataMapsOrThrow } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/delete-field-from-flat-object-metadata-maps-or-throw.util'; @@ -42,6 +42,7 @@ export class ObjectMetadataServiceV2 { private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, + private readonly viewV2Service: ViewV2Service, ) {} async updateOne({ @@ -96,7 +97,7 @@ export class ObjectMetadataServiceV2 { toFlatObjectMetadataMaps, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -198,7 +199,7 @@ export class ObjectMetadataServiceV2 { fromFlatObjectMetadataMaps, toFlatObjectMetadataMaps, buildOptions: { - inferDeletionFromMissingObjectFieldIndex: true, + inferDeletionFromMissingEntities: true, isSystemBuild: false, }, workspaceId, @@ -288,7 +289,7 @@ export class ObjectMetadataServiceV2 { toFlatObjectMetadataMaps, buildOptions: { isSystemBuild: false, - inferDeletionFromMissingObjectFieldIndex: false, + inferDeletionFromMissingEntities: false, }, workspaceId, }, @@ -318,26 +319,6 @@ export class ObjectMetadataServiceV2 { ); } - const [view] = await this.viewRepository.find({ - where: { - objectMetadataId: createdFlatObjectMetadata.id, - key: ViewKey.INDEX, - workspaceId, - }, - }); - - if (!isDefined(view)) { - throw new ObjectMetadataException( - 'View not created', - ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - await this.objectMetadataRelatedRecordsService.createViewWorkspaceFavorite( - workspaceId, - view.id, - ); - return createdFlatObjectMetadata; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception.ts new file mode 100644 index 0000000000..05a379e096 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception.ts @@ -0,0 +1,6 @@ +export class WorkspaceMigrationOrchestratorException extends Error { + constructor(message = 'Workspace migration orchestrator failed') { + super(message); + this.name = 'WorkspaceMigrationOrchestratorException'; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts new file mode 100644 index 0000000000..093f8e55be --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service.ts @@ -0,0 +1,189 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; + +import { ViewCacheService } from 'src/engine/core-modules/view/cache/services/view-cache.service'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; +import { WorkspaceMigrationOrchestratorException } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-orchestrator-exception'; +import { + WorkspaceMigrationOrchestratorBuildArgs, + WorkspaceMigrationOrchestratorEntityMaps, + WorkspaceMigrationOrchestratorFailedResult, + WorkspaceMigrationOrchestratorOptimisticEntityMaps, +} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; +import { WorkspaceViewFieldMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service'; +import { WorkspaceViewMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service'; +import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service'; +import { + WorkspaceMigrationV2Exception, + WorkspaceMigrationV2ExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration.exception'; + +@Injectable() +export class WorkspaceMigrationBuildOrchestratorService { + private readonly logger = new Logger( + WorkspaceMigrationBuildOrchestratorService.name, + ); + + constructor( + private readonly workspaceMigrationBuilderV2Service: WorkspaceMigrationBuilderV2Service, + private readonly workspaceViewMigrationBuilderV2Service: WorkspaceViewMigrationBuilderV2Service, + private readonly workspaceViewFieldMigrationBuilderV2Service: WorkspaceViewFieldMigrationBuilderV2Service, + private readonly migrationRunner: WorkspaceMigrationRunnerV2Service, + private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, + private readonly viewCacheService: ViewCacheService, + ) {} + + public async buildWorkspaceMigrations({ + workspaceId, + buildOptions, + entityMaps, + }: WorkspaceMigrationOrchestratorBuildArgs): Promise< + WorkspaceMigrationOrchestratorFailedResult | undefined + > { + try { + const allObjectAndFieldActions = []; + const allViewActions = []; + const allViewFieldActions = []; + const allFailures = []; + + const optimisticEntityMaps = await this.prepareOptimisticEntityMaps({ + entityMaps, + workspaceId, + }); + + if (entityMaps.object) { + const objectResult = + await this.workspaceMigrationBuilderV2Service.validateAndBuild({ + fromFlatObjectMetadataMaps: + entityMaps.object.fromFlatObjectMetadataMaps, + toFlatObjectMetadataMaps: + entityMaps.object.toFlatObjectMetadataMaps, + workspaceId, + buildOptions, + }); + + if (objectResult.status === 'fail') { + allFailures.push(...objectResult.errors); + } else { + optimisticEntityMaps.object = objectResult.optimisticFlatEntityMaps; + allObjectAndFieldActions.push( + ...objectResult.workspaceMigration.actions, + ); + } + } + + if (entityMaps.view) { + const viewResult = + await this.workspaceViewMigrationBuilderV2Service.validateAndBuild({ + fromFlatViewMaps: entityMaps.view.fromFlatViewMaps, + toFlatViewMaps: entityMaps.view.toFlatViewMaps, + workspaceId, + buildOptions, + dependencyOptimisticEntityMaps: { + object: optimisticEntityMaps.object, + }, + }); + + if (viewResult.status === 'fail') { + allFailures.push(...viewResult.errors); + } else { + optimisticEntityMaps.view = viewResult.optimisticFlatEntityMaps; + allViewActions.push(...viewResult.workspaceMigration.actions); + } + } + + if (entityMaps.viewField) { + const viewFieldResult = + await this.workspaceViewFieldMigrationBuilderV2Service.validateAndBuild( + { + fromFlatViewFieldMaps: entityMaps.viewField.fromFlatViewFieldMaps, + toFlatViewFieldMaps: entityMaps.viewField.toFlatViewFieldMaps, + workspaceId, + buildOptions, + dependencyOptimisticEntityMaps: { + object: optimisticEntityMaps.object, + view: optimisticEntityMaps.view, + }, + }, + ); + + if (viewFieldResult.status === 'fail') { + allFailures.push(...viewFieldResult.errors); + } else { + optimisticEntityMaps.viewField = + viewFieldResult.optimisticFlatEntityMaps; + allViewFieldActions.push( + ...viewFieldResult.workspaceMigration.actions, + ); + } + } + + if (allFailures.length > 0) { + return { + status: 'fail', + errors: allFailures, + }; + } + + // TODO: return workspace migrations + + return; + } catch (error) { + this.logger.error(error); + throw new WorkspaceMigrationV2Exception( + WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR, + error.message, + ); + } + } + + // TODO: move to dedicated service + private async prepareOptimisticEntityMaps({ + entityMaps, + workspaceId, + }: { + entityMaps: WorkspaceMigrationOrchestratorEntityMaps; + workspaceId: string; + }): Promise { + const optimisticObjectMaps = + entityMaps.object?.fromFlatObjectMetadataMaps ?? + ( + await this.workspaceMetadataCacheService.getExistingOrRecomputeFlatObjectMetadataMaps( + { workspaceId }, + ) + ).flatObjectMetadataMaps; + + if (!isDefined(optimisticObjectMaps)) { + throw new WorkspaceMigrationOrchestratorException( + 'Object metadata maps not found', + ); + } + + const optimisticViewMaps = + entityMaps.view?.fromFlatViewMaps ?? + ( + await this.viewCacheService.getExistingOrRecomputeFlatViewMaps({ + workspaceId, + }) + ).flatViewMaps; + + if (!isDefined(optimisticViewMaps)) { + throw new WorkspaceMigrationOrchestratorException('View maps not found'); + } + + // For ViewFields, if not provided, start with empty maps since ViewFields are optional + const optimisticViewFieldMaps = entityMaps.viewField + ?.fromFlatViewFieldMaps ?? { + byId: {}, + idByUniversalIdentifier: {}, + }; + + return { + object: optimisticObjectMaps, + view: optimisticViewMaps, + viewField: optimisticViewFieldMaps, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts index 9075629f4c..56523f8b66 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service.ts @@ -11,6 +11,7 @@ import { WorkspaceMigrationV2ExceptionCode, } from 'src/engine/workspace-manager/workspace-migration.exception'; +// TODO: replace build logic by orchestrator service @Injectable() export class WorkspaceMigrationValidateBuildAndRunService { private readonly logger = new Logger( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type.ts deleted file mode 100644 index 6d65839fba..0000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; - -export type FlatViewField = Pick< - ViewFieldEntity, - 'fieldMetadataId' | 'position' | 'isVisible' | 'size' ->; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type.ts deleted file mode 100644 index d81b0502b0..0000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; -import { type FlatViewField } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-view-field.type'; - -export type FlatView = Pick< - ViewEntity, - 'name' | 'objectMetadataId' | 'type' | 'isCustom' | 'key' | 'icon' -> & { - viewFields: FlatViewField[]; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type.ts new file mode 100644 index 0000000000..ec77744db0 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type.ts @@ -0,0 +1,41 @@ +import { type FromTo } from 'twenty-shared/types'; + +import { type FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type'; +import { type FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type'; +import { type FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { type FailedFlatFieldMetadataValidation } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type'; +import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; +import { type FailedFlatObjectMetadataValidation } from 'src/engine/metadata-modules/flat-object-metadata/types/failed-flat-object-metadata-validation.type'; +import { type WorkspaceMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; + +export type WorkspaceMigrationOrchestratorEntityMaps = { + object?: FromTo; + field?: FromTo; // Fields are part of object maps for now + view?: FromTo; + viewField?: FromTo; +}; + +type UnwrapFromTo = T extends FromTo ? U : T; + +export type WorkspaceMigrationOrchestratorOptimisticEntityMaps = { + [K in keyof WorkspaceMigrationOrchestratorEntityMaps]?: UnwrapFromTo< + WorkspaceMigrationOrchestratorEntityMaps[K] + >; +}; + +export type WorkspaceMigrationOrchestratorBuildArgs = { + workspaceId: string; + buildOptions: WorkspaceMigrationV2BuilderOptions; + entityMaps: WorkspaceMigrationOrchestratorEntityMaps; +}; + +export type WorkspaceMigrationOrchestratorFailedResult = { + status: 'fail'; + errors: ( + | FailedFlatObjectMetadataValidation + | FailedFlatFieldMetadataValidation + | FailedFlatViewValidation + | FailedFlatViewFieldValidation + )[]; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap index 6edf49fe4a..0bde343ccb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap @@ -1033,85 +1033,6 @@ exports[`Workspace migration builder object actions test suite It should build a }, "type": "create_object", }, - { - "type": "create_view", - "view": { - "icon": "IconList", - "isCustom": false, - "key": "INDEX", - "name": "All {objectLabelPlural}", - "objectMetadataId": Any, - "type": "TABLE", - "viewFields": [ - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 0, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 1, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 2, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 3, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 4, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 5, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 6, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 7, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 8, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 9, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 10, - "size": 180, - }, - ], - }, - }, ] `; @@ -2419,181 +2340,6 @@ exports[`Workspace migration builder object actions test suite It should build a }, "type": "create_object", }, - { - "type": "create_view", - "view": { - "icon": "IconList", - "isCustom": false, - "key": "INDEX", - "name": "All {objectLabelPlural}", - "objectMetadataId": Any, - "type": "TABLE", - "viewFields": [ - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 0, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 1, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 2, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 3, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 4, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 5, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 6, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 7, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 8, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 9, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 10, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 11, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 12, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 13, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 14, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 15, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 16, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 17, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 18, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 19, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 20, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 21, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 22, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 23, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 24, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 25, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 26, - "size": 180, - }, - ], - }, - }, ] `; @@ -3246,85 +2992,6 @@ exports[`Workspace migration builder object actions test suite It should build a }, "type": "create_index", }, - { - "type": "create_view", - "view": { - "icon": "IconList", - "isCustom": false, - "key": "INDEX", - "name": "All {objectLabelPlural}", - "objectMetadataId": Any, - "type": "TABLE", - "viewFields": [ - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 0, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 1, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 2, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 3, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 4, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 5, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 6, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 7, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 8, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 9, - "size": 180, - }, - { - "fieldMetadataId": Any, - "isVisible": true, - "position": 10, - "size": 180, - }, - ], - }, - }, ] `; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts index e718ac05ed..5fe98548f4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case.ts @@ -44,7 +44,7 @@ const DELETE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ context: { input: { buildOptions: { - inferDeletionFromMissingObjectFieldIndex: true, + inferDeletionFromMissingEntities: true, isSystemBuild: true, }, fromFlatObjectMetadataMaps: FLAT_OBJECT_METADATA_MAPS_MOCKS, @@ -81,7 +81,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ }, expectedActionsTypeCounter: { createObject: 1, - createView: 1, }, }, }, @@ -103,7 +102,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ }, expectedActionsTypeCounter: { createObject: 1, - createView: 1, }, }, }, @@ -114,7 +112,6 @@ const CREATE_OBJECT_TEST_CASES: WorkspaceMigrationBuilderTestCase[] = [ expectedActionsTypeCounter: { createIndex: 1, createObject: 1, - createView: 1, }, input: { fromFlatObjectMetadataMaps: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-builder.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-builder.spec.ts index 14c5a5ae17..d595a5c6dd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-builder.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-builder.spec.ts @@ -54,7 +54,6 @@ const expectedActionsTypeCounterChecker = ({ deleteObject: 0, updateField: 0, updateObject: 0, - createView: 0, }; const actualActionsTypeCounter = workspaceMigration.actions.reduce( (acc, action) => { @@ -125,7 +124,7 @@ describe.each(allWorkspaceBuilderTestCases)( fromFlatObjectMetadataMaps, toFlatObjectMetadataMaps, buildOptions = { - inferDeletionFromMissingObjectFieldIndex: true, + inferDeletionFromMissingEntities: true, isSystemBuild: false, }, } = typeof input === 'function' ? input() : input; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service.ts index 0b477aab88..b4b602eee7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service.ts @@ -155,7 +155,7 @@ export class WorkspaceMigrationV2FieldActionsBuilderService { validateAndBuildResult.created.push(createFieldAction); } - for (const flatFieldMetadataToDelete of buildOptions.inferDeletionFromMissingObjectFieldIndex + for (const flatFieldMetadataToDelete of buildOptions.inferDeletionFromMissingEntities ? deletedFlatFieldMetadatas : []) { const validationErrors = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts index 5762500c2e..dd808b564b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service.ts @@ -103,7 +103,7 @@ export class WorkspaceMigrationV2ObjectActionsBuilderService { validateAndBuildResult.created.push(createObjectAction); } - for (const flatObjectMetadataToDelete of buildOptions.inferDeletionFromMissingObjectFieldIndex + for (const flatObjectMetadataToDelete of buildOptions.inferDeletionFromMissingEntities ? deletedFlatObjectMetadatas : []) { const validationErrors = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service.ts new file mode 100644 index 0000000000..16ccf7153c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service.ts @@ -0,0 +1,178 @@ +import { Injectable } from '@nestjs/common'; + +import { FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type'; +import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { compareTwoFlatViewField } from 'src/engine/core-modules/view/flat-view/utils/compare-two-flat-view-field.util'; +import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { type CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util'; +import { WorkspaceViewFieldMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service'; +import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2'; +import { + UpdateViewFieldAction, + WorkspaceMigrationViewFieldActionV2, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; +import { + getWorkspaceMigrationV2ViewFieldCreateAction, + getWorkspaceMigrationV2ViewFieldDeleteAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action'; +import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service'; + +export type CreatedDeletedUpdatedViewFieldInputMatrix = FromTo< + FlatViewFieldMaps, + 'FlatViewFieldMaps' +> & + CustomDeletedCreatedUpdatedMatrix<'flatViewField', FlatViewField> & { + buildOptions: WorkspaceViewFieldMigrationV2BuilderOptions; + fromFlatViewFieldMaps: FlatViewFieldMaps; + dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps; + }; + +type ValidateAndBuildViewFieldResult = { + failed: FailedFlatViewFieldValidation[]; + created: T[]; + deleted: T[]; + updated: T[]; + optimisticFlatViewFieldMaps: FlatViewFieldMaps; +}; + +@Injectable() +export class WorkspaceMigrationV2ViewFieldActionsBuilderService { + constructor( + private readonly flatViewFieldValidatorService: FlatViewFieldValidatorService, + ) {} + + public async validateAndBuildViewFieldActions({ + createdFlatViewField, + deletedFlatViewField, + updatedFlatViewField, + buildOptions, + fromFlatViewFieldMaps, + dependencyOptimisticEntityMaps, + }: CreatedDeletedUpdatedViewFieldInputMatrix): Promise< + ValidateAndBuildViewFieldResult + > { + const validateAndBuildResult: ValidateAndBuildViewFieldResult = + { + failed: [], + created: [], + deleted: [], + updated: [], + optimisticFlatViewFieldMaps: structuredClone(fromFlatViewFieldMaps), + }; + + if ( + !isDefined(dependencyOptimisticEntityMaps.object) || + !isDefined(dependencyOptimisticEntityMaps.view) + ) { + throw new Error('Dependency optimistic entity maps are not defined'); + } + + for (const flatViewFieldToCreate of createdFlatViewField) { + const validationErrors = + await this.flatViewFieldValidatorService.validateFlatViewFieldCreation({ + _existingFlatViewFieldMaps: + validateAndBuildResult.optimisticFlatViewFieldMaps, + flatViewFieldToValidate: flatViewFieldToCreate, + optimisticFlatObjectMetadataMaps: + dependencyOptimisticEntityMaps.object, + optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view, + }); + + if (validationErrors.viewFieldLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewFieldMaps = + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatViewFieldToCreate, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps, + }); + + const createViewFieldAction = + getWorkspaceMigrationV2ViewFieldCreateAction({ + flatViewField: flatViewFieldToCreate, + }); + + validateAndBuildResult.created.push(createViewFieldAction); + } + + for (const flatViewFieldToDelete of buildOptions.inferDeletionFromMissingEntities + ? deletedFlatViewField + : []) { + const validationErrors = + this.flatViewFieldValidatorService.validateFlatViewFieldDeletion({ + existingFlatViewFieldMaps: + validateAndBuildResult.optimisticFlatViewFieldMaps, + viewFieldIdToDelete: flatViewFieldToDelete.id, + optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view, + }); + + if (validationErrors.viewFieldLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewFieldMaps = + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatViewFieldToDelete.id, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps, + }); + + const deleteViewFieldAction = + getWorkspaceMigrationV2ViewFieldDeleteAction(flatViewFieldToDelete); + + validateAndBuildResult.deleted.push(deleteViewFieldAction); + } + + for (const { + from: fromFlatViewField, + to: toFlatViewField, + } of updatedFlatViewField) { + const viewFieldUpdatedProperties = compareTwoFlatViewField({ + fromFlatViewField, + toFlatViewField, + }); + + if (viewFieldUpdatedProperties.length === 0) { + continue; + } + + const validationErrors = + this.flatViewFieldValidatorService.validateFlatViewFieldUpdate({ + existingFlatViewFieldMaps: + validateAndBuildResult.optimisticFlatViewFieldMaps, + updatedFlatViewField: toFlatViewField, + optimisticFlatViewMaps: dependencyOptimisticEntityMaps.view, + }); + + if (validationErrors.viewFieldLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewFieldMaps = + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: toFlatViewField, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewFieldMaps, + }); + + const updateViewFieldAction: UpdateViewFieldAction = { + type: 'update_view_field', + viewFieldId: toFlatViewField.id, + updates: viewFieldUpdatedProperties, + }; + + validateAndBuildResult.updated.push(updateViewFieldAction); + } + + return validateAndBuildResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service.ts new file mode 100644 index 0000000000..bbd92f4318 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service.ts @@ -0,0 +1,165 @@ +import { Injectable } from '@nestjs/common'; + +import { FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util'; +import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/core-modules/common/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util'; +import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; +import { compareTwoFlatView } from 'src/engine/core-modules/view/flat-view/utils/compare-two-flat-view.util'; +import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { type CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util'; +import { WorkspaceViewMigrationV2BuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service'; +import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2'; +import { + UpdateViewAction, + WorkspaceMigrationViewActionV2, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { + getWorkspaceMigrationV2ViewCreateAction, + getWorkspaceMigrationV2ViewDeleteAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action'; +import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service'; + +export type CreatedDeletedUpdatedViewInputMatrix = FromTo< + FlatViewMaps, + 'FlatViewMaps' +> & + CustomDeletedCreatedUpdatedMatrix<'flatView', FlatView> & { + buildOptions: WorkspaceViewMigrationV2BuilderOptions; + fromFlatViewMaps: FlatViewMaps; + dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps; + }; + +type ValidateAndBuildViewResult = { + failed: FailedFlatViewValidation[]; + created: T[]; + deleted: T[]; + updated: T[]; + optimisticFlatViewMaps: FlatViewMaps; +}; + +@Injectable() +export class WorkspaceMigrationV2ViewActionsBuilderService { + constructor( + private readonly flatViewValidatorService: FlatViewValidatorService, + ) {} + + public async validateAndBuildViewActions({ + createdFlatView, + deletedFlatView, + updatedFlatView, + buildOptions, + fromFlatViewMaps, + dependencyOptimisticEntityMaps, + }: CreatedDeletedUpdatedViewInputMatrix): Promise< + ValidateAndBuildViewResult + > { + const validateAndBuildResult: ValidateAndBuildViewResult = + { + failed: [], + created: [], + deleted: [], + updated: [], + optimisticFlatViewMaps: structuredClone(fromFlatViewMaps), + }; + + if (!isDefined(dependencyOptimisticEntityMaps.object)) { + throw new Error('Dependency optimistic entity maps are not defined'); + } + + for (const flatViewToCreate of createdFlatView) { + const validationErrors = + await this.flatViewValidatorService.validateFlatViewCreation({ + _existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps, + flatViewToValidate: flatViewToCreate, + optimisticFlatObjectMetadataMaps: + dependencyOptimisticEntityMaps.object, + }); + + if (validationErrors.viewLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewMaps = + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: flatViewToCreate, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps, + }); + + const createViewAction = getWorkspaceMigrationV2ViewCreateAction({ + flatView: flatViewToCreate, + }); + + validateAndBuildResult.created.push(createViewAction); + } + + for (const flatViewToDelete of buildOptions.inferDeletionFromMissingEntities + ? deletedFlatView + : []) { + const validationErrors = + this.flatViewValidatorService.validateFlatViewDeletion({ + existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps, + viewIdToDelete: flatViewToDelete.id, + }); + + if (validationErrors.viewLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewMaps = + deleteFlatEntityFromFlatEntityMapsOrThrow({ + entityToDeleteId: flatViewToDelete.id, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps, + }); + + const deleteViewAction = + getWorkspaceMigrationV2ViewDeleteAction(flatViewToDelete); + + validateAndBuildResult.deleted.push(deleteViewAction); + } + + for (const { from: fromFlatView, to: toFlatView } of updatedFlatView) { + const viewUpdatedProperties = compareTwoFlatView({ + fromFlatView, + toFlatView, + }); + + if (viewUpdatedProperties.length === 0) { + continue; + } + + const validationErrors = + this.flatViewValidatorService.validateFlatViewUpdate({ + existingFlatViewMaps: validateAndBuildResult.optimisticFlatViewMaps, + updatedFlatView: toFlatView, + }); + + if (validationErrors.viewLevelErrors.length > 0) { + validateAndBuildResult.failed.push(validationErrors); + continue; + } + + validateAndBuildResult.optimisticFlatViewMaps = + replaceFlatEntityInFlatEntityMapsOrThrow({ + flatEntity: toFlatView, + flatEntityMaps: validateAndBuildResult.optimisticFlatViewMaps, + }); + + const updateViewAction: UpdateViewAction = { + type: 'update_view', + viewId: toFlatView.id, + updates: viewUpdatedProperties, + }; + + validateAndBuildResult.updated.push(updateViewAction); + } + + return validateAndBuildResult; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.ts deleted file mode 100644 index 2b93e82478..0000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; -import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; -import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { - type CreateViewAction, - type WorkspaceMigrationViewActionV2, -} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; - -type BuildWorkspaceMigrationViewActionsArgs = { - createdFlatObjectMetadatas: FlatObjectMetadata[]; -}; - -const getCreateViewAction = ( - flatObjectMetadata: FlatObjectMetadata, -): CreateViewAction => { - return { - type: 'create_view', - view: { - objectMetadataId: flatObjectMetadata.id, - type: ViewType.TABLE, - name: `All {objectLabelPlural}`, - key: ViewKey.INDEX, - isCustom: false, - icon: 'IconList', - viewFields: flatObjectMetadata.flatFieldMetadatas - .filter((field) => field.name !== 'id' && field.name !== 'deletedAt') - .map((field, index) => ({ - fieldMetadataId: field.id, - position: index, - isVisible: true, - size: 180, - })), - }, - }; -}; - -export const buildWorkspaceMigrationViewActions = ({ - createdFlatObjectMetadatas, -}: BuildWorkspaceMigrationViewActionsArgs): WorkspaceMigrationViewActionV2[] => { - return createdFlatObjectMetadatas.map(getCreateViewAction); -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts new file mode 100644 index 0000000000..175ba6057b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service.ts @@ -0,0 +1,102 @@ +import { type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/core-modules/common/types/flat-entity-maps.type'; +import { type FlatEntity } from 'src/engine/core-modules/common/types/flat-entity.type'; +import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util'; +import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2'; +import { type WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2'; + +export type SuccessfulEntityMigrationBuildResult = { + status: 'success'; + workspaceMigration: WorkspaceMigrationV2; + optimisticFlatEntityMaps: TFlatEntityMaps; +}; + +export type FailedEntityMigrationBuildResult = { + status: 'fail'; + errors: TFailedValidation[]; +}; + +export abstract class WorkspaceEntityMigrationBuilderV2Service< + TFlatEntityMaps extends FlatEntityMaps, + TFlatEntity extends FlatEntity, + TFailedValidation, + TActionType extends WorkspaceMigrationActionV2, + TBuildArgs extends { workspaceId: string } & FromTo, +> { + public async validateAndBuild( + args: TBuildArgs, + ): Promise< + | SuccessfulEntityMigrationBuildResult + | FailedEntityMigrationBuildResult + > { + const { from: fromEntityMaps, to: toEntityMaps } = + this.extractFromToEntityMaps(args); + + const fromEntities = Object.values(fromEntityMaps.byId).filter(isDefined); + const toEntities = Object.values(toEntityMaps.byId).filter(isDefined); + + const { created, deleted, updated } = deletedCreatedUpdatedMatrixDispatcher( + { + from: fromEntities, + to: toEntities, + }, + ); + + const result = await this.validateAndBuildActions({ + ...args, + created, + updated, + deleted, + }); + + if (result.failed.length > 0) { + return { + status: 'fail', + errors: result.failed, + }; + } + + return { + status: 'success', + workspaceMigration: { + workspaceId: args.workspaceId, + actions: [...result.deleted, ...result.created, ...result.updated], + }, + optimisticFlatEntityMaps: result.optimisticMaps, + }; + } + + // TODO: Refactor, this is too hacky + private extractFromToEntityMaps(args: TBuildArgs): { + from: TFlatEntityMaps; + to: TFlatEntityMaps; + } { + const fromKey = Object.keys(args).find((key) => + key.startsWith('from'), + ) as keyof TBuildArgs; + const toKey = Object.keys(args).find((key) => + key.startsWith('to'), + ) as keyof TBuildArgs; + + return { + from: args[fromKey] as TFlatEntityMaps, + to: args[toKey] as TFlatEntityMaps, + }; + } + + protected abstract validateAndBuildActions( + args: TBuildArgs & { + created: TFlatEntity[]; + updated: { from: TFlatEntity; to: TFlatEntity }[]; + deleted: TFlatEntity[]; + }, + ): Promise<{ + failed: TFailedValidation[]; + created: TActionType[]; + deleted: TActionType[]; + updated: TActionType[]; + optimisticMaps: TFlatEntityMaps; + }>; +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts index 52632e6018..1a1d65dd37 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service.ts @@ -10,20 +10,20 @@ import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-mana import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service'; import { buildWorkspaceMigrationIndexActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder'; import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service'; -import { buildWorkspaceMigrationViewActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder'; import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2'; import { computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util'; import { computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util'; import { getWorkspaceMigrationV2CreateIndexAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-index-actions'; export type WorkspaceMigrationV2BuilderOptions = { - inferDeletionFromMissingObjectFieldIndex: boolean; + inferDeletionFromMissingEntities: boolean; isSystemBuild: boolean; }; type SuccessfulWorkspaceMigrationBuildResult = { status: 'success'; workspaceMigration: WorkspaceMigrationV2; + optimisticFlatEntityMaps: FlatObjectMetadataMaps; }; export type FailedWorkspaceMigrationBuildResult = { @@ -71,11 +71,13 @@ export class WorkspaceMigrationBuilderV2Service { to: toFlatObjectMetadatas, }); + // Should be handled separately from objects const objectMetadataDeletedCreatedUpdatedIndex = computeUpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix( updatedFlatObjectMetadatas, ); + // Should be handled separately from objects const objectMetadataDeletedCreatedUpdatedFields = computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix( updatedFlatObjectMetadatas, @@ -116,12 +118,7 @@ export class WorkspaceMigrationBuilderV2Service { const indexWorkspaceMigrationActions = buildWorkspaceMigrationIndexActions({ objectMetadataDeletedCreatedUpdatedIndex, inferDeletionFromMissingObjectFieldIndex: - buildOptions.inferDeletionFromMissingObjectFieldIndex, - }); - - // TODO: Remove this from here when we have a proper builder that handles views diffing independently - const viewWorkspaceMigrationActions = buildWorkspaceMigrationViewActions({ - createdFlatObjectMetadatas, // should be flatViews, not objects. + buildOptions.inferDeletionFromMissingEntities, }); const allValidateAndBuildResultFailures = [ @@ -141,17 +138,18 @@ export class WorkspaceMigrationBuilderV2Service { workspaceMigration: { workspaceId, actions: [ - ...fieldActionsValidateAndBuildResult.deleted, + ...fieldActionsValidateAndBuildResult.deleted, // Should be handled separately from objects ...objectActionsValidateAndBuildResult.deleted, ...objectActionsValidateAndBuildResult.created, ...objectActionsValidateAndBuildResult.updated, - ...fieldActionsValidateAndBuildResult.created, - ...fieldActionsValidateAndBuildResult.updated, + ...fieldActionsValidateAndBuildResult.created, // Should be handled separately from objects + ...fieldActionsValidateAndBuildResult.updated, // Should be handled separately from objects ...createdObjectMetadataCreateIndexActions, - ...indexWorkspaceMigrationActions, - ...viewWorkspaceMigrationActions, + ...indexWorkspaceMigrationActions, // Should be handled separately from objects ], }, + optimisticFlatEntityMaps: + fieldActionsValidateAndBuildResult.optimisticFlatObjectMetadataMaps, }; } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service.ts new file mode 100644 index 0000000000..29a6059ee7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; + +import { type FromTo } from 'twenty-shared/types'; + +import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type'; +import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service'; +import { + FailedEntityMigrationBuildResult, + WorkspaceEntityMigrationBuilderV2Service, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service'; +import { WorkspaceMigrationViewFieldActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; + +export type WorkspaceViewFieldMigrationV2BuilderOptions = { + isSystemBuild: boolean; + inferDeletionFromMissingEntities: boolean; +}; + +export type FailedWorkspaceViewFieldMigrationBuildResult = + FailedEntityMigrationBuildResult; + +export type WorkspaceViewFieldMigrationBuildArgs = { + workspaceId: string; + buildOptions: WorkspaceViewFieldMigrationV2BuilderOptions; + dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps; +} & FromTo; + +@Injectable() +export class WorkspaceViewFieldMigrationBuilderV2Service extends WorkspaceEntityMigrationBuilderV2Service< + FlatViewFieldMaps, + FlatViewField, + FailedFlatViewFieldValidation, + WorkspaceMigrationViewFieldActionV2, + WorkspaceViewFieldMigrationBuildArgs +> { + constructor( + private readonly workspaceMigrationV2ViewFieldActionsBuilderService: WorkspaceMigrationV2ViewFieldActionsBuilderService, + ) { + super(); + } + + protected async validateAndBuildActions( + args: WorkspaceViewFieldMigrationBuildArgs & { + created: FlatViewField[]; + updated: { from: FlatViewField; to: FlatViewField }[]; + deleted: FlatViewField[]; + }, + ): Promise<{ + failed: FailedFlatViewFieldValidation[]; + created: WorkspaceMigrationViewFieldActionV2[]; + deleted: WorkspaceMigrationViewFieldActionV2[]; + updated: WorkspaceMigrationViewFieldActionV2[]; + optimisticMaps: FlatViewFieldMaps; + }> { + const viewFieldActionsValidateAndBuildResult = + await this.workspaceMigrationV2ViewFieldActionsBuilderService.validateAndBuildViewFieldActions( + { + fromFlatViewFieldMaps: args.fromFlatViewFieldMaps, + toFlatViewFieldMaps: args.toFlatViewFieldMaps, + createdFlatViewField: args.created, + updatedFlatViewField: args.updated, + deletedFlatViewField: args.deleted, + buildOptions: args.buildOptions, + dependencyOptimisticEntityMaps: args.dependencyOptimisticEntityMaps, + }, + ); + + return { + failed: viewFieldActionsValidateAndBuildResult.failed, + created: viewFieldActionsValidateAndBuildResult.created, + deleted: viewFieldActionsValidateAndBuildResult.deleted, + updated: viewFieldActionsValidateAndBuildResult.updated, + optimisticMaps: + viewFieldActionsValidateAndBuildResult.optimisticFlatViewFieldMaps, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service.ts new file mode 100644 index 0000000000..58930c75f1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; + +import { type FromTo } from 'twenty-shared/types'; + +import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; +import { WorkspaceMigrationOrchestratorOptimisticEntityMaps } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service'; +import { + FailedEntityMigrationBuildResult, + WorkspaceEntityMigrationBuilderV2Service, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service'; +import { WorkspaceMigrationViewActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; + +export type WorkspaceViewMigrationV2BuilderOptions = { + isSystemBuild: boolean; + inferDeletionFromMissingEntities: boolean; +}; + +export type FailedWorkspaceViewMigrationBuildResult = + FailedEntityMigrationBuildResult; + +export type WorkspaceViewMigrationBuildArgs = { + workspaceId: string; + buildOptions: WorkspaceViewMigrationV2BuilderOptions; + dependencyOptimisticEntityMaps: WorkspaceMigrationOrchestratorOptimisticEntityMaps; +} & FromTo; + +@Injectable() +export class WorkspaceViewMigrationBuilderV2Service extends WorkspaceEntityMigrationBuilderV2Service< + FlatViewMaps, + FlatView, + FailedFlatViewValidation, + WorkspaceMigrationViewActionV2, + WorkspaceViewMigrationBuildArgs +> { + constructor( + private readonly workspaceMigrationV2ViewActionsBuilderService: WorkspaceMigrationV2ViewActionsBuilderService, + ) { + super(); + } + + protected async validateAndBuildActions( + args: WorkspaceViewMigrationBuildArgs & { + created: FlatView[]; + updated: { from: FlatView; to: FlatView }[]; + deleted: FlatView[]; + }, + ): Promise<{ + failed: FailedFlatViewValidation[]; + created: WorkspaceMigrationViewActionV2[]; + deleted: WorkspaceMigrationViewActionV2[]; + updated: WorkspaceMigrationViewActionV2[]; + optimisticMaps: FlatViewMaps; + }> { + const viewActionsValidateAndBuildResult = + await this.workspaceMigrationV2ViewActionsBuilderService.validateAndBuildViewActions( + { + fromFlatViewMaps: args.fromFlatViewMaps, + toFlatViewMaps: args.toFlatViewMaps, + createdFlatView: args.created, + updatedFlatView: args.updated, + deletedFlatView: args.deleted, + buildOptions: args.buildOptions, + dependencyOptimisticEntityMaps: args.dependencyOptimisticEntityMaps, + }, + ); + + return { + failed: viewActionsValidateAndBuildResult.failed, + created: viewActionsValidateAndBuildResult.created, + deleted: viewActionsValidateAndBuildResult.deleted, + updated: viewActionsValidateAndBuildResult.updated, + optimisticMaps: viewActionsValidateAndBuildResult.optimisticFlatViewMaps, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-view-result.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-view-result.type.ts new file mode 100644 index 0000000000..df53c0fbf1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/validate-and-build-view-result.type.ts @@ -0,0 +1,11 @@ +import { type FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type'; +import { type FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { type WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2'; + +export type ValidateAndBuildViewResult = { + failed: FailedFlatViewValidation[]; + created: T[]; + deleted: T[]; + updated: T[]; + optimisticFlatViewMaps: FlatViewMaps; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2.ts index 1e3430f166..2c111cfb11 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2.ts @@ -2,12 +2,14 @@ import { type WorkspaceMigrationFieldActionV2 } from 'src/engine/workspace-manag import { type WorkspaceMigrationIndexActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2'; import { type WorkspaceMigrationObjectActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; import { type WorkspaceMigrationViewActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { type WorkspaceMigrationViewFieldActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; export type WorkspaceMigrationActionV2 = | WorkspaceMigrationObjectActionV2 | WorkspaceMigrationFieldActionV2 | WorkspaceMigrationIndexActionV2 - | WorkspaceMigrationViewActionV2; + | WorkspaceMigrationViewActionV2 + | WorkspaceMigrationViewFieldActionV2; export type WorkspaceMigrationActionTypeV2 = WorkspaceMigrationActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type.ts index 6d9b398586..3055286be7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type.ts @@ -1,11 +1,37 @@ -import { type FlatView } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-view.type'; +import { type FromTo } from 'twenty-shared/types'; + +import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { type FlatViewPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-properties-to-compare.type'; +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; + +export type FlatViewPropertyUpdate

= { + property: P; +} & FromTo; export type CreateViewAction = { type: 'create_view'; view: FlatView; }; -export type WorkspaceMigrationViewActionV2 = CreateViewAction; +export type UpdateViewAction = { + type: 'update_view'; + viewId: string; + updates: Array< + { + [P in FlatViewPropertiesToCompare]: FlatViewPropertyUpdate

; + }[FlatViewPropertiesToCompare] + >; +}; + +export type DeleteViewAction = { + type: 'delete_view'; + viewId: string; +}; + +export type WorkspaceMigrationViewActionV2 = + | CreateViewAction + | UpdateViewAction + | DeleteViewAction; export type WorkspaceMigrationViewActionTypeV2 = WorkspaceMigrationViewActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type.ts new file mode 100644 index 0000000000..b8e9baefc4 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type.ts @@ -0,0 +1,39 @@ +import { type FromTo } from 'twenty-shared/types'; + +import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { type FlatViewFieldPropertiesToCompare } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-properties-to-compare.type'; +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; + +export type FlatViewFieldPropertyUpdate< + P extends FlatViewFieldPropertiesToCompare, +> = { + property: P; +} & FromTo; + +export type CreateViewFieldAction = { + type: 'create_view_field'; + viewField: FlatViewField; +}; + +export type UpdateViewFieldAction = { + type: 'update_view_field'; + viewFieldId: string; + updates: Array< + { + [P in FlatViewFieldPropertiesToCompare]: FlatViewFieldPropertyUpdate

; + }[FlatViewFieldPropertiesToCompare] + >; +}; + +export type DeleteViewFieldAction = { + type: 'delete_view_field'; + viewFieldId: string; +}; + +export type WorkspaceMigrationViewFieldActionV2 = + | CreateViewFieldAction + | UpdateViewFieldAction + | DeleteViewFieldAction; + +export type WorkspaceMigrationViewFieldActionTypeV2 = + WorkspaceMigrationViewFieldActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action.ts new file mode 100644 index 0000000000..cc98ae1095 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-action.ts @@ -0,0 +1,21 @@ +import { type FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; +import { + type CreateViewAction, + type DeleteViewAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; + +export const getWorkspaceMigrationV2ViewCreateAction = ({ + flatView, +}: { + flatView: FlatView; +}): CreateViewAction => ({ + type: 'create_view', + view: flatView, +}); + +export const getWorkspaceMigrationV2ViewDeleteAction = ( + flatView: FlatView, +): DeleteViewAction => ({ + type: 'delete_view', + viewId: flatView.id, +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action.ts new file mode 100644 index 0000000000..949ebcd59b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-view-field-action.ts @@ -0,0 +1,21 @@ +import { type FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { + type CreateViewFieldAction, + type DeleteViewFieldAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; + +export const getWorkspaceMigrationV2ViewFieldCreateAction = ({ + flatViewField, +}: { + flatViewField: FlatViewField; +}): CreateViewFieldAction => ({ + type: 'create_view_field', + viewField: flatViewField, +}); + +export const getWorkspaceMigrationV2ViewFieldDeleteAction = ( + flatViewField: FlatViewField, +): DeleteViewFieldAction => ({ + type: 'delete_view_field', + viewFieldId: flatViewField.id, +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service.ts new file mode 100644 index 0000000000..605b725fd5 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service.ts @@ -0,0 +1,151 @@ +import { Injectable } from '@nestjs/common'; + +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception'; +import { FailedFlatViewFieldValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-field-validation.type'; +import { FlatViewFieldMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-field-maps.type'; +import { FlatViewField } from 'src/engine/core-modules/view/flat-view/types/flat-view-field.type'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; +import { findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId } from 'src/engine/metadata-modules/flat-object-metadata-maps/utils/find-flat-field-metadata-in-flat-object-metadata-maps-with-field-id-only.util'; + +@Injectable() +export class FlatViewFieldValidatorService { + constructor() {} + + public validateFlatViewFieldUpdate({ + existingFlatViewFieldMaps, + updatedFlatViewField, + optimisticFlatViewMaps, + }: { + existingFlatViewFieldMaps: FlatViewFieldMaps; + updatedFlatViewField: FlatViewField; + optimisticFlatViewMaps: FlatViewMaps; + }): FailedFlatViewFieldValidation { + const errors = []; + + const optimisticFlatView = + optimisticFlatViewMaps.byId[updatedFlatViewField.viewId]; + + if (!isDefined(optimisticFlatView)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: t`View not found`, + }); + } + + const existingFlatViewField = + existingFlatViewFieldMaps.byId[updatedFlatViewField.id]; + + if (!isDefined(existingFlatViewField)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field not found`, + userFriendlyMessage: t`View field not found`, + }); + } + + return { + type: 'update_view_field', + viewFieldLevelErrors: errors, + failedViewFieldValidationMinimalInformation: { + id: updatedFlatViewField.id, + viewId: updatedFlatViewField.viewId, + fieldMetadataId: updatedFlatViewField.fieldMetadataId, + }, + }; + } + + public validateFlatViewFieldDeletion({ + existingFlatViewFieldMaps, + viewFieldIdToDelete, + optimisticFlatViewMaps, + }: { + existingFlatViewFieldMaps: FlatViewFieldMaps; + viewFieldIdToDelete: string; + optimisticFlatViewMaps: FlatViewMaps; + }): FailedFlatViewFieldValidation { + const errors = []; + + const optimisticFlatView = optimisticFlatViewMaps.byId[viewFieldIdToDelete]; + + if (!isDefined(optimisticFlatView)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: t`View not found`, + }); + } + + const existingFlatViewField = + existingFlatViewFieldMaps.byId[viewFieldIdToDelete]; + + if (!isDefined(existingFlatViewField)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View field not found`, + userFriendlyMessage: t`View field not found`, + }); + } + + return { + type: 'delete_view_field', + viewFieldLevelErrors: errors, + failedViewFieldValidationMinimalInformation: { + id: viewFieldIdToDelete, + }, + }; + } + + public async validateFlatViewFieldCreation({ + _existingFlatViewFieldMaps, + flatViewFieldToValidate, + optimisticFlatObjectMetadataMaps, + optimisticFlatViewMaps, + }: { + _existingFlatViewFieldMaps: FlatViewFieldMaps; + flatViewFieldToValidate: FlatViewField; + optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps; + optimisticFlatViewMaps: FlatViewMaps; + }): Promise { + const errors = []; + + const relatedFlatFieldMetadata = + findFlatFieldMetadataInFlatObjectMetadataMapsWithOnlyFieldId({ + fieldMetadataId: flatViewFieldToValidate.fieldMetadataId, + flatObjectMetadataMaps: optimisticFlatObjectMetadataMaps, + }); + + if (!isDefined(relatedFlatFieldMetadata)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Field metadata not found`, + userFriendlyMessage: t`Field metadata not found`, + }); + } + + const optimisticFlatView = + optimisticFlatViewMaps.byId[flatViewFieldToValidate.viewId]; + + if (!isDefined(optimisticFlatView)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: t`View not found`, + }); + } + + return { + type: 'create_view_field', + viewFieldLevelErrors: errors, + failedViewFieldValidationMinimalInformation: { + id: flatViewFieldToValidate.id, + viewId: flatViewFieldToValidate.viewId, + fieldMetadataId: flatViewFieldToValidate.fieldMetadataId, + }, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts new file mode 100644 index 0000000000..805e8945a5 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@nestjs/common'; + +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; + +import { ViewExceptionCode } from 'src/engine/core-modules/view/exceptions/view.exception'; +import { FailedFlatViewValidation } from 'src/engine/core-modules/view/flat-view/types/failed-flat-view-validation.type'; +import { FlatViewMaps } from 'src/engine/core-modules/view/flat-view/types/flat-view-maps.type'; +import { FlatView } from 'src/engine/core-modules/view/flat-view/types/flat-view.type'; +import { FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type'; + +@Injectable() +export class FlatViewValidatorService { + constructor() {} + + public validateFlatViewUpdate({ + existingFlatViewMaps, + updatedFlatView, + }: { + existingFlatViewMaps: FlatViewMaps; + updatedFlatView: FlatView; + }): FailedFlatViewValidation { + const errors = []; + + const existingFlatView = existingFlatViewMaps.byId[updatedFlatView.id]; + + if (!isDefined(existingFlatView)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: t`View not found`, + }); + } + + return { + type: 'update_view', + viewLevelErrors: errors, + failedViewValidationMinimalInformation: { + id: updatedFlatView.id, + }, + }; + } + + public validateFlatViewDeletion({ + existingFlatViewMaps, + viewIdToDelete, + }: { + existingFlatViewMaps: FlatViewMaps; + viewIdToDelete: string; + }): FailedFlatViewValidation { + const errors = []; + + const existingFlatView = existingFlatViewMaps.byId[viewIdToDelete]; + + if (!isDefined(existingFlatView)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`View not found`, + userFriendlyMessage: t`View not found`, + }); + } + + return { + type: 'delete_view', + viewLevelErrors: errors, + failedViewValidationMinimalInformation: { + id: viewIdToDelete, + }, + }; + } + + public async validateFlatViewCreation({ + _existingFlatViewMaps, + flatViewToValidate, + optimisticFlatObjectMetadataMaps, + }: { + _existingFlatViewMaps: FlatViewMaps; + flatViewToValidate: FlatView; + optimisticFlatObjectMetadataMaps: FlatObjectMetadataMaps; + }): Promise { + const optimisticFlatObjectMetadata = + optimisticFlatObjectMetadataMaps.byId[ + flatViewToValidate.objectMetadataId + ]; + + const errors = []; + + if (!isDefined(optimisticFlatObjectMetadata)) { + errors.push({ + code: ViewExceptionCode.INVALID_VIEW_DATA, + message: t`Object metadata not found`, + userFriendlyMessage: t`Object metadata not found`, + }); + } + + return { + type: 'create_view', + viewLevelErrors: errors, + failedViewValidationMinimalInformation: { + id: flatViewToValidate.id, + }, + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts new file mode 100644 index 0000000000..a404b86b5a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service'; +import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-validator.service'; + +@Module({ + providers: [FlatViewValidatorService, FlatViewFieldValidatorService], + exports: [FlatViewValidatorService, FlatViewFieldValidatorService], +}) +export class WorkspaceMigrationBuilderValidatorsModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts index eb6e406c1f..8d3b9710cb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts @@ -6,10 +6,15 @@ import { FlatFieldMetadataValidatorService } from 'src/engine/metadata-modules/f import { FlatObjectMetadataValidatorService } from 'src/engine/metadata-modules/flat-object-metadata/services/flat-object-metadata-validator.service'; import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/services/workspace-migration-v2-field-actions-builder.service'; import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/services/workspace-migration-v2-object-actions-builder.service'; +import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service'; +import { WorkspaceMigrationV2ViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view/workspace-migration-v2-view-actions-builder.service'; import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-migration-builder-v2.service'; +import { WorkspaceViewFieldMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-field-migration-builder-v2.service'; +import { WorkspaceViewMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-view-migration-builder-v2.service'; +import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/workspace-migration-builder-validators.module'; @Module({ - imports: [FeatureFlagModule], + imports: [FeatureFlagModule, WorkspaceMigrationBuilderValidatorsModule], providers: [ WorkspaceMigrationBuilderV2Service, FlatFieldMetadataValidatorService, @@ -17,7 +22,15 @@ import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager WorkspaceMigrationV2ObjectActionsBuilderService, FlatObjectMetadataValidatorService, WorkspaceMigrationV2FieldActionsBuilderService, + WorkspaceMigrationV2ViewActionsBuilderService, + WorkspaceViewMigrationBuilderV2Service, + WorkspaceViewFieldMigrationBuilderV2Service, + WorkspaceMigrationV2ViewFieldActionsBuilderService, + ], + exports: [ + WorkspaceMigrationBuilderV2Service, + WorkspaceViewMigrationBuilderV2Service, + WorkspaceViewFieldMigrationBuilderV2Service, ], - exports: [WorkspaceMigrationBuilderV2Service], }) export class WorkspaceMigrationBuilderV2Module {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service.ts new file mode 100644 index 0000000000..a179f6b034 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { CreateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class CreateViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'create_view_field', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { viewField } = action; + + const viewFieldRepository = + queryRunner.manager.getRepository(ViewFieldEntity); + + await viewFieldRepository.save({ + ...viewField, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service.ts new file mode 100644 index 0000000000..5d47d3f744 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { DeleteViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete_view_field', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { viewFieldId } = action; + + const viewFieldRepository = + queryRunner.manager.getRepository(ViewFieldEntity); + + await viewFieldRepository.delete({ + id: viewFieldId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service.ts new file mode 100644 index 0000000000..316bb7933b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; +import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util'; + +@Injectable() +export class UpdateViewFieldActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update_view_field', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner } = context; + const { viewFieldId } = action; + + const viewFieldRepository = + queryRunner.manager.getRepository(ViewEntity); + + await viewFieldRepository.update( + viewFieldId, + fromWorkspaceMigrationUpdateActionToPartialEntity(action), + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service.ts index fe80a6628a..41e6aee225 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; -import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; import { CreateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; @@ -21,25 +20,13 @@ export class CreateViewActionHandlerService extends WorkspaceMigrationRunnerActi const { action, queryRunner, workspaceId } = context; const { view } = action; - const { viewFields } = view; - const viewRepository = queryRunner.manager.getRepository(ViewEntity); - const viewFieldRepository = - queryRunner.manager.getRepository(ViewFieldEntity); - const createdView = await viewRepository.save({ + await viewRepository.save({ ...view, workspaceId, }); - - await viewFieldRepository.save( - viewFields.map((viewField) => ({ - ...viewField, - viewId: createdView.id, - workspaceId, - })), - ); } async executeForWorkspaceSchema( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service.ts new file mode 100644 index 0000000000..c460ac496e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { DeleteViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; + +@Injectable() +export class DeleteViewActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'delete_view', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner, workspaceId } = context; + const { viewId } = action; + + const viewRepository = + queryRunner.manager.getRepository(ViewEntity); + + await viewRepository.delete({ + id: viewId, + workspaceId, + }); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service.ts new file mode 100644 index 0000000000..ea72bca105 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface'; + +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type'; +import { fromWorkspaceMigrationUpdateActionToPartialEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util'; + +@Injectable() +export class UpdateViewActionHandlerService extends WorkspaceMigrationRunnerActionHandler( + 'update_view', +) { + constructor() { + super(); + } + + async executeForMetadata( + context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + const { action, queryRunner } = context; + const { viewId } = action; + + const viewRepository = + queryRunner.manager.getRepository(ViewEntity); + + await viewRepository.update( + viewId, + fromWorkspaceMigrationUpdateActionToPartialEntity(action), + ); + } + + async executeForWorkspaceSchema( + _context: WorkspaceMigrationActionRunnerArgs, + ): Promise { + return; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index f4e8e93199..35c2e543f5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -9,7 +9,12 @@ import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/wo import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service'; import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service'; import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service'; +import { CreateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/create-view-field-action-handler.service'; +import { DeleteViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/delete-view-field-action-handler.service'; +import { UpdateViewFieldActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view-field/services/update-view-field-action-handler.service'; import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/create-view-action-handler.service'; +import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/delete-view-action-handler.service'; +import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/view/services/update-view-action-handler.service'; @Module({ imports: [WorkspaceSchemaManagerModule], @@ -26,6 +31,12 @@ import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/wor DeleteIndexActionHandlerService, CreateViewActionHandlerService, + UpdateViewActionHandlerService, + DeleteViewActionHandlerService, + + CreateViewFieldActionHandlerService, + UpdateViewFieldActionHandlerService, + DeleteViewFieldActionHandlerService, ], }) export class WorkspaceSchemaMigrationRunnerActionHandlersModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/apply-workspace-migration-action-on-flat-object-metadata-maps.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/apply-workspace-migration-action-on-flat-object-metadata-maps.ts index 1a93ba7003..65aa62f102 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/apply-workspace-migration-action-on-flat-object-metadata-maps.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/apply-workspace-migration-action-on-flat-object-metadata-maps.ts @@ -135,6 +135,11 @@ export const applyWorkspaceMigrationActionOnFlatObjectMetadataMaps = < case 'create_index': case 'delete_index': case 'create_view': + case 'update_view': + case 'delete_view': + case 'create_view_field': + case 'update_view_field': + case 'delete_view_field': return flatObjectMetadataMaps; default: { assertUnreachable( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util.ts index c4299ced4f..ecd12a70bf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-workspace-migration-update-action-to-partial-field-or-object-entity.util.ts @@ -1,8 +1,14 @@ import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2'; import { type UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2'; +import { type UpdateViewAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-action-v2.type'; +import { type UpdateViewFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-view-field-action-v2.type'; export const fromWorkspaceMigrationUpdateActionToPartialEntity = < - T extends UpdateFieldAction | UpdateObjectAction, + T extends + | UpdateFieldAction + | UpdateObjectAction + | UpdateViewAction + | UpdateViewFieldAction, >( action: T, ) => { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts index f7f50bb489..f326158ef6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; +import { ViewCacheModule } from 'src/engine/core-modules/view/cache/services/view-cache.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; +import { WorkspaceMigrationBuildOrchestratorService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-build-orchestrator.service'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module'; import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module'; @@ -8,8 +11,16 @@ import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/w imports: [ WorkspaceMigrationBuilderV2Module, WorkspaceMigrationRunnerV2Module, + WorkspaceMetadataCacheModule, + ViewCacheModule, + ], + providers: [ + WorkspaceMigrationValidateBuildAndRunService, + WorkspaceMigrationBuildOrchestratorService, + ], + exports: [ + WorkspaceMigrationValidateBuildAndRunService, + WorkspaceMigrationBuildOrchestratorService, ], - providers: [WorkspaceMigrationValidateBuildAndRunService], - exports: [WorkspaceMigrationValidateBuildAndRunService], }) export class WorkspaceMigrationV2Module {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface.ts index d09b12a8ff..f97298050e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync/interfaces/syncable-entity.interface.ts @@ -5,5 +5,5 @@ import { Column, Index } from 'typeorm'; }) export abstract class SyncableEntity { @Column({ nullable: true, type: 'uuid' }) - universalIdentifier?: string | null; + universalIdentifier: string | null; }