From 2a28c34a37d355b70938442a48f5f468781a03bc Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:05:21 +0100 Subject: [PATCH] Refactor workspace migration runner exception handling (#17310) # Introduction In this PR we catch all the runner errors coming from a single workspace migration action execution. **1. `WorkspaceMigrationActionExecutionException`** (low-level, action-specific) - Thrown from action handlers, utils, and helper functions - Contains specific error codes: `FIELD_METADATA_NOT_FOUND`, `OBJECT_METADATA_NOT_FOUND`, `ENUM_OPERATION_FAILED`, `NOT_SUPPORTED`, etc. - Simple structure: `message`, `code`, `userFriendlyMessage` - No action context - just describes what went wrong **2. `WorkspaceMigrationRunnerException`** (high-level, runner-scoped) - Only two codes: `INTERNAL_SERVER_ERROR` and `EXECUTION_FAILED` - `EXECUTION_FAILED` **requires** `action` + `errors` (contains the action context) - `INTERNAL_SERVER_ERROR` **requires** `message` (no action context) ## Refactor - Removed the `relatedFlatEntityMapsKeys` from the `WorkspaceMigration` type as they're directly inferred from passed actions - Swallowing actions rollbacks errors in order to iterate over all of them ## Testing Created a very straigthforward install application from workspace migration endpoint in order to start testing the introduced `WorkspaceMigrationActionExecutionException` Introduced a feature flag that stop the access to the endpoint if not enabled Whole taken direction are totally subjective and highly prone to mutations ( endpoint location, naming and input schema see `ts-expect-error` comment ) cc @martmull ## Response error ```ts { "eventId": "evt_a1b2c3d4-5678-90ab-cdef-1234567890ab", "extensions": { "action": { "metadataName": "fieldMetadata", "type": "delete", "universalIdentifier": "20202020-6110-4547-9fd0-2525257a2c3f" }, "code": "APPLICATION_INSTALLATION_FAILED", "errors": { "metadata": { "code": "ENTITY_NOT_FOUND", "message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f" }, "workspaceSchema": { "code": "ENTITY_NOT_FOUND", "message": "Could not find flat entity in maps" } }, "exceptionEventId": "exc_f9e8d7c6-5432-10ba-fedc-ba0987654321", "userFriendlyMessage": "Migration execution failed." }, "message": "Migration action 'delete' for 'fieldMetadata' failed", "name": "GraphQLError" } ``` --- .../src/generated-metadata/graphql.ts | 50 +++++ .../twenty-front/src/generated/graphql.ts | 50 +++++ .../application/application-sync.module.ts | 12 +- .../application/application.resolver.ts | 64 +++++-- .../dtos/install-application.input.ts | 54 ++++++ .../enums/feature-flag-key.enum.ts | 1 + .../graphql/utils/graphql-errors.util.ts | 1 + .../ai/ai-agent/agent.resolver.ts | 4 +- .../ai/ai-agent/ai-agent.module.ts | 4 +- .../command-menu-item.module.ts | 4 +- .../command-menu-item.resolver.ts | 4 +- .../front-component/front-component.module.ts | 4 +- .../front-component.resolver.ts | 4 +- .../navigation-menu-item.module.ts | 4 +- .../navigation-menu-item.resolver.ts | 4 +- .../page-layout-tab/page-layout-tab.module.ts | 4 +- .../resolvers/page-layout-tab.resolver.ts | 4 +- .../page-layout-widget.module.ts | 4 +- .../resolvers/page-layout-widget.resolver.ts | 4 +- .../page-layout/page-layout.module.ts | 4 +- .../resolvers/page-layout.resolver.ts | 4 +- .../metadata-modules/role/role.module.ts | 4 +- .../metadata-modules/role/role.resolver.ts | 4 +- .../metadata-modules/skill/skill.module.ts | 4 +- .../metadata-modules/skill/skill.resolver.ts | 4 +- .../workspace-entity-manager.spec.ts | 1 + ...tion-graphql-api-exception.interceptor.ts} | 8 +- ...ce-migration-runner-exception-formatter.ts | 47 +++++ ...ce-migration-build-orchestrator.service.ts | 8 - ...igration-validate-build-and-run-service.ts | 12 +- ...orkspace-migration-action-type.constant.ts | 5 + ...-create-workspace-migration-action.type.ts | 3 +- ...-delete-workspace-migration-action.type.ts | 4 +- ...-update-workspace-migration-action.type.ts | 3 +- .../types/workspace-migration.ts | 3 - .../update-field-action-handler.service.ts | 32 ++-- .../index/utils/index-action-handler.utils.ts | 16 +- ...verless-function-action-handler.service.ts | 5 +- ...ce-migration-action-execution.exception.ts | 64 +++++++ .../workspace-migration-runner.exception.ts | 94 +++++----- ...runner-action-handler-service.interface.ts | 64 +++++-- ...-runner-action-handler-registry.service.ts | 55 +++--- .../workspace-migration-runner.service.ts | 174 +++++++++--------- ...kspace-schema-enum-operations.util.spec.ts | 4 +- ...field-metadata-type-to-column-type.util.ts | 14 +- .../utils/generate-column-definitions.util.ts | 14 +- .../workspace-schema-enum-operations.util.ts | 14 +- ...ure-flag-disabled.integration-spec.ts.snap | 13 ++ ...stall-application.integration-spec.ts.snap | 29 +++ ...-feature-flag-disabled.integration-spec.ts | 23 +++ ...ng-install-application.integration-spec.ts | 44 +++++ .../install-application-query-factory.util.ts | 26 +++ .../utils/install-application.util.ts | 40 ++++ 53 files changed, 829 insertions(+), 294 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/application/dtos/install-application.input.ts rename packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/{workspace-migration-builder-graphql-api-exception.interceptor.ts => workspace-migration-graphql-api-exception.interceptor.ts} (70%) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application-feature-flag-disabled.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/failing-install-application-feature-flag-disabled.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/install-application-query-factory.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/utils/install-application.util.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index af8b0d1018..93bb22fb1c 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -183,6 +183,33 @@ export enum AggregateOperations { SUM = 'SUM' } +export enum AllMetadataName { + agent = 'agent', + commandMenuItem = 'commandMenuItem', + cronTrigger = 'cronTrigger', + databaseEventTrigger = 'databaseEventTrigger', + fieldMetadata = 'fieldMetadata', + frontComponent = 'frontComponent', + index = 'index', + navigationMenuItem = 'navigationMenuItem', + objectMetadata = 'objectMetadata', + pageLayout = 'pageLayout', + pageLayoutTab = 'pageLayoutTab', + pageLayoutWidget = 'pageLayoutWidget', + role = 'role', + roleTarget = 'roleTarget', + routeTrigger = 'routeTrigger', + rowLevelPermissionPredicate = 'rowLevelPermissionPredicate', + rowLevelPermissionPredicateGroup = 'rowLevelPermissionPredicateGroup', + serverlessFunction = 'serverlessFunction', + skill = 'skill', + view = 'view', + viewField = 'viewField', + viewFilter = 'viewFilter', + viewFilterGroup = 'viewFilterGroup', + viewGroup = 'viewGroup' +} + export type Analytics = { __typename?: 'Analytics'; /** Boolean that confirms query was dispatched */ @@ -1453,6 +1480,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', + IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', @@ -2102,6 +2130,7 @@ export type Mutation = { impersonate: ImpersonateOutput; initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; + installApplication: Scalars['Boolean']; publishServerlessFunction: ServerlessFunction; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; @@ -2759,6 +2788,11 @@ export type MutationInitiateOtpProvisioningArgs = { }; +export type MutationInstallApplicationArgs = { + workspaceMigration: WorkspaceMigrationInput; +}; + + export type MutationPublishServerlessFunctionArgs = { input: PublishServerlessFunctionInput; }; @@ -5582,6 +5616,22 @@ export enum WorkspaceMemberTimeFormatEnum { SYSTEM = 'SYSTEM' } +export enum WorkspaceMigrationActionType { + create = 'create', + delete = 'delete', + update = 'update' +} + +export type WorkspaceMigrationDeleteActionInput = { + metadataName: AllMetadataName; + type: WorkspaceMigrationActionType; + universalIdentifier: Scalars['String']; +}; + +export type WorkspaceMigrationInput = { + actions: Array; +}; + export type WorkspaceNameAndId = { __typename?: 'WorkspaceNameAndId'; displayName?: Maybe; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 47b6981124..e1caa639ad 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -183,6 +183,33 @@ export enum AggregateOperations { SUM = 'SUM' } +export enum AllMetadataName { + agent = 'agent', + commandMenuItem = 'commandMenuItem', + cronTrigger = 'cronTrigger', + databaseEventTrigger = 'databaseEventTrigger', + fieldMetadata = 'fieldMetadata', + frontComponent = 'frontComponent', + index = 'index', + navigationMenuItem = 'navigationMenuItem', + objectMetadata = 'objectMetadata', + pageLayout = 'pageLayout', + pageLayoutTab = 'pageLayoutTab', + pageLayoutWidget = 'pageLayoutWidget', + role = 'role', + roleTarget = 'roleTarget', + routeTrigger = 'routeTrigger', + rowLevelPermissionPredicate = 'rowLevelPermissionPredicate', + rowLevelPermissionPredicateGroup = 'rowLevelPermissionPredicateGroup', + serverlessFunction = 'serverlessFunction', + skill = 'skill', + view = 'view', + viewField = 'viewField', + viewFilter = 'viewFilter', + viewFilterGroup = 'viewFilterGroup', + viewGroup = 'viewGroup' +} + export type Analytics = { __typename?: 'Analytics'; /** Boolean that confirms query was dispatched */ @@ -1420,6 +1447,7 @@ export type FeatureFlagDto = { export enum FeatureFlagKey { IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', + IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', @@ -2059,6 +2087,7 @@ export type Mutation = { impersonate: ImpersonateOutput; initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; + installApplication: Scalars['Boolean']; publishServerlessFunction: ServerlessFunction; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; @@ -2662,6 +2691,11 @@ export type MutationInitiateOtpProvisioningArgs = { }; +export type MutationInstallApplicationArgs = { + workspaceMigration: WorkspaceMigrationInput; +}; + + export type MutationPublishServerlessFunctionArgs = { input: PublishServerlessFunctionInput; }; @@ -5357,6 +5391,22 @@ export enum WorkspaceMemberTimeFormatEnum { SYSTEM = 'SYSTEM' } +export enum WorkspaceMigrationActionType { + create = 'create', + delete = 'delete', + update = 'update' +} + +export type WorkspaceMigrationDeleteActionInput = { + metadataName: AllMetadataName; + type: WorkspaceMigrationActionType; + universalIdentifier: Scalars['String']; +}; + +export type WorkspaceMigrationInput = { + actions: Array; +}; + export type WorkspaceNameAndId = { __typename?: 'WorkspaceNameAndId'; displayName?: Maybe; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts index 4759c5b08d..0b9c2927c8 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.module.ts @@ -5,6 +5,8 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver'; import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module'; +import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; +import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module'; @@ -18,11 +20,11 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { RouteTriggerModule } from 'src/engine/metadata-modules/route-trigger/route-trigger.module'; import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module'; import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; -import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; @Module({ imports: [ @@ -45,11 +47,13 @@ import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; PermissionFlagModule, WorkflowCommonModule, FileStorageModule, + WorkspaceCacheModule, + WorkspaceMigrationRunnerModule, ], providers: [ ApplicationResolver, ApplicationSyncService, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [ApplicationSyncService], }) diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts index 5ec160319c..a4c01a9da6 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -1,4 +1,9 @@ -import { UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; +import { + UseFilters, + UseGuards, + UseInterceptors, + UsePipes, +} from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; @@ -6,47 +11,54 @@ import path, { join } from 'path'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import { PermissionFlagType } from 'twenty-shared/constants'; -import { type Repository } from 'typeorm'; import { FileFolder } from 'twenty-shared/types'; +import { type Repository } from 'typeorm'; import type { FileUpload } from 'graphql-upload/processRequest.mjs'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter'; import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service'; +import { + ApplicationException, + ApplicationExceptionCode, +} from 'src/engine/core-modules/application/application.exception'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input'; +import { InstallApplicationInput } from 'src/engine/core-modules/application/dtos/install-application.input'; import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput'; +import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; +import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; +import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { - ApplicationException, - ApplicationExceptionCode, -} from 'src/engine/core-modules/application/application.exception'; -import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; @UseGuards( WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.APPLICATIONS), ) +@UsePipes(ResolverValidationPipe) @Resolver() -@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @UseFilters(ApplicationExceptionFilter) export class ApplicationResolver { constructor( + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly applicationSyncService: ApplicationSyncService, private readonly applicationService: ApplicationService, private readonly fileStorageService: FileStorageService, + private readonly workspaceCacheService: WorkspaceCacheService, @InjectRepository(FileEntity) private readonly fileRepository: Repository, ) {} @@ -83,6 +95,36 @@ export class ApplicationResolver { return true; } + @Mutation(() => Boolean) + async installApplication( + @Args() { workspaceMigration: { actions } }: InstallApplicationInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + ) { + const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute( + workspaceId, + ['featureFlagsMap'], + ); + + if ( + featureFlagsMap[ + FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED + ] !== true + ) { + throw new ApplicationException( + 'Application installation from tarball is not enabled', + ApplicationExceptionCode.FORBIDDEN, + ); + } + + await this.workspaceMigrationRunnerService.run({ + // @ts-expect-error ignoring until we implement a production ready validation schema for workspaceMigration + actions, + workspaceId, + }); + + return true; + } + @Mutation(() => Boolean) async uninstallApplication( @Args() { universalIdentifier }: UninstallApplicationInput, diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/install-application.input.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/install-application.input.ts new file mode 100644 index 0000000000..7b51882416 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/install-application.input.ts @@ -0,0 +1,54 @@ +import { ArgsType, Field, InputType, registerEnumType } from '@nestjs/graphql'; + +import { Type } from 'class-transformer'; +import { + ArrayNotEmpty, + IsArray, + IsEnum, + IsUUID, + ValidateNested, +} from 'class-validator'; +import { ALL_METADATA_NAME, AllMetadataName } from 'twenty-shared/metadata'; + +import { WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; + +registerEnumType(ALL_METADATA_NAME, { + name: 'AllMetadataName', +}); + +registerEnumType(WORKSPACE_MIGRATION_ACTION_TYPE, { + name: 'WorkspaceMigrationActionType', +}); + +@InputType() +export class WorkspaceMigrationDeleteActionInput { + @Field(() => WORKSPACE_MIGRATION_ACTION_TYPE) + @IsEnum(WORKSPACE_MIGRATION_ACTION_TYPE) + type: 'delete'; + + @Field(() => ALL_METADATA_NAME) + @IsEnum(ALL_METADATA_NAME) + metadataName: AllMetadataName; + + @Field(() => String) + @IsUUID() + universalIdentifier: string; +} + +@InputType() +export class WorkspaceMigrationInput { + @Field(() => [WorkspaceMigrationDeleteActionInput]) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => WorkspaceMigrationDeleteActionInput) + actions: WorkspaceMigrationDeleteActionInput[]; +} + +@ArgsType() +export class InstallApplicationInput { + @Field(() => WorkspaceMigrationInput) + @ValidateNested() + @Type(() => WorkspaceMigrationInput) + workspaceMigration: WorkspaceMigrationInput; +} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 0386cf6956..b3a217ff64 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -3,6 +3,7 @@ export enum FeatureFlagKey { IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_AI_ENABLED = 'IS_AI_ENABLED', IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', + IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', IS_PAGE_LAYOUT_ENABLED = 'IS_PAGE_LAYOUT_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index a45e5a26e7..aa2761ad3b 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -32,6 +32,7 @@ export enum ErrorCode { TIMEOUT = 'TIMEOUT', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', METADATA_VALIDATION_FAILED = 'METADATA_VALIDATION_FAILED', + APPLICATION_INSTALLATION_FAILED = 'APPLICATION_INSTALLATION_FAILED', } type RestrictedGraphQLErrorExtensions = { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.resolver.ts index 4b5ec56ada..b2afeb2194 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/agent.resolver.ts @@ -13,7 +13,7 @@ import { import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { AgentService } from './agent.service'; @@ -29,7 +29,7 @@ import { AgentGraphqlApiExceptionInterceptor } from './interceptors/agent-graphq SettingsPermissionGuard(PermissionFlagType.AI), ) @UseInterceptors( - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, AgentGraphqlApiExceptionInterceptor, ) @Resolver() diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts index 74fb409ad1..a0ea7e492b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts @@ -17,7 +17,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { AgentResolver } from './agent.resolver'; @@ -46,7 +46,7 @@ import { AgentEntity } from './entities/agent.entity'; providers: [ AgentResolver, AgentService, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, AgentGraphqlApiExceptionInterceptor, ], exports: [AgentService, TypeOrmModule.forFeature([AgentEntity])], diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.module.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.module.ts index 6499e54a4e..a47a417b8f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.module.ts @@ -7,7 +7,7 @@ import { CommandMenuItemService } from 'src/engine/metadata-modules/command-menu import { CommandMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/command-menu-item/interceptors/command-menu-item-graphql-api-exception.interceptor'; import { FlatCommandMenuItemModule } from 'src/engine/metadata-modules/flat-command-menu-item/flat-command-menu-item.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -22,7 +22,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace CommandMenuItemService, CommandMenuItemResolver, CommandMenuItemGraphqlApiExceptionInterceptor, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [CommandMenuItemService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts index cd3ac0505b..68c1c00c28 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/command-menu-item.resolver.ts @@ -16,11 +16,11 @@ import { CommandMenuItemDTO } from 'src/engine/metadata-modules/command-menu-ite import { CreateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/create-command-menu-item.input'; import { UpdateCommandMenuItemInput } from 'src/engine/metadata-modules/command-menu-item/dtos/update-command-menu-item.input'; import { CommandMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/command-menu-item/interceptors/command-menu-item-graphql-api-exception.interceptor'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @UseGuards(WorkspaceAuthGuard, FeatureFlagGuard) @UseInterceptors( - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, CommandMenuItemGraphqlApiExceptionInterceptor, ) @Resolver(() => CommandMenuItemDTO) diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts index cd4d9b3806..769835481d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts @@ -7,7 +7,7 @@ import { FrontComponentResolver } from 'src/engine/metadata-modules/front-compon import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -22,7 +22,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace FrontComponentService, FrontComponentResolver, FrontComponentGraphqlApiExceptionInterceptor, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [FrontComponentService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts index a17006880c..b74139e235 100644 --- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts @@ -14,11 +14,11 @@ import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/d import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input'; import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service'; import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @UseGuards(WorkspaceAuthGuard) @UseInterceptors( - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, FrontComponentGraphqlApiExceptionInterceptor, ) @Resolver(() => FrontComponentDTO) diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts index 39e587035f..358a77136a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module.ts @@ -8,7 +8,7 @@ import { NavigationMenuItemResolver } from 'src/engine/metadata-modules/navigati import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service'; import { NavigationMenuItemAccessService } from 'src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-access.service'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -24,7 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace NavigationMenuItemAccessService, NavigationMenuItemResolver, NavigationMenuItemGraphqlApiExceptionInterceptor, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [NavigationMenuItemService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts index 10cb848d27..d8d101c825 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.resolver.ts @@ -15,11 +15,11 @@ import { NavigationMenuItemDTO } from 'src/engine/metadata-modules/navigation-me import { UpdateOneNavigationMenuItemInput } from 'src/engine/metadata-modules/navigation-menu-item/dtos/update-navigation-menu-item.input'; import { NavigationMenuItemGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/navigation-menu-item/interceptors/navigation-menu-item-graphql-api-exception.interceptor'; import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @UseGuards(WorkspaceAuthGuard) @UseInterceptors( - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, NavigationMenuItemGraphqlApiExceptionInterceptor, ) @Resolver(() => NavigationMenuItemDTO) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/page-layout-tab.module.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/page-layout-tab.module.ts index 9d359db9ae..16708d7a2f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/page-layout-tab.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/page-layout-tab.module.ts @@ -14,7 +14,7 @@ import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-ta import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.module'; @@ -36,7 +36,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m providers: [ PageLayoutTabService, PageLayoutTabResolver, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [PageLayoutTabService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver.ts index 161081008f..d0310ce703 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver.ts @@ -19,10 +19,10 @@ import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layou import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto'; import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service'; import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @Resolver(() => PageLayoutTabDTO) -@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @UseFilters(PageLayoutGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) @UsePipes(ResolverValidationPipe) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/page-layout-widget.module.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/page-layout-widget.module.ts index 3632f5de34..91c15d634d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/page-layout-widget.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/page-layout-widget.module.ts @@ -13,7 +13,7 @@ import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.module'; @@ -34,7 +34,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m providers: [ PageLayoutWidgetService, PageLayoutWidgetResolver, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [PageLayoutWidgetService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver.ts index acbed6f856..dfcb6561a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver.ts @@ -27,10 +27,10 @@ import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-wid import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface'; import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service'; import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @Resolver(() => PageLayoutWidgetDTO) -@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @UseFilters(PageLayoutGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) @UsePipes(ResolverValidationPipe) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts index 333adaa38d..e555a03ce5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/page-layout.module.ts @@ -18,7 +18,7 @@ import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/servi import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.module'; @@ -44,7 +44,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m PageLayoutDuplicationService, PageLayoutResolver, PageLayoutUpdateService, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [PageLayoutService, PageLayoutDuplicationService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts index b6e3bcaa03..d61c33af67 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/resolvers/page-layout.resolver.ts @@ -21,10 +21,10 @@ import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page import { PageLayoutUpdateService } from 'src/engine/metadata-modules/page-layout/services/page-layout-update.service'; import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service'; import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @Resolver(() => PageLayoutDTO) -@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) @UseFilters(PageLayoutGraphqlApiExceptionFilter) @UseGuards(WorkspaceAuthGuard) @UsePipes(ResolverValidationPipe) diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index f94d8c0860..877455ea99 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -28,7 +28,7 @@ import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/r import { RowLevelPermissionModule } from 'src/engine/metadata-modules/row-level-permission-predicate/row-level-permission.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -65,7 +65,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace RoleResolver, WorkspaceFlatRoleMapCacheService, WorkspaceFlatRoleTargetMapCacheService, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, WorkspaceRolesPermissionsCacheService, ], exports: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index 70c31d34af..386b470dd8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -64,7 +64,7 @@ import { UpsertRowLevelPermissionPredicatesResultDTO } from 'src/engine/metadata import { RowLevelPermissionPredicateGroupService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate-group.service'; import { RowLevelPermissionPredicateService } from 'src/engine/metadata-modules/row-level-permission-predicate/services/row-level-permission-predicate.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Resolver(() => RoleDTO) @@ -77,7 +77,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta PermissionsGraphqlApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter, ) -@UseInterceptors(WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor) +@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor) export class RoleResolver { constructor( private readonly userRoleService: UserRoleService, diff --git a/packages/twenty-server/src/engine/metadata-modules/skill/skill.module.ts b/packages/twenty-server/src/engine/metadata-modules/skill/skill.module.ts index 90022caaf9..3d2ac11976 100644 --- a/packages/twenty-server/src/engine/metadata-modules/skill/skill.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/skill/skill.module.ts @@ -7,7 +7,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { SkillGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/skill/interceptors/skill-graphql-api-exception.interceptor'; import { SkillResolver } from 'src/engine/metadata-modules/skill/skill.resolver'; import { SkillService } from 'src/engine/metadata-modules/skill/skill.service'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ @@ -22,7 +22,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace SkillService, SkillResolver, SkillGraphqlApiExceptionInterceptor, - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, ], exports: [SkillService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/skill/skill.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/skill/skill.resolver.ts index a9fb454991..9ed1e3612c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/skill/skill.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/skill/skill.resolver.ts @@ -13,11 +13,11 @@ import { SkillDTO } from 'src/engine/metadata-modules/skill/dtos/skill.dto'; import { UpdateSkillInput } from 'src/engine/metadata-modules/skill/dtos/update-skill.input'; import { SkillGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/skill/interceptors/skill-graphql-api-exception.interceptor'; import { SkillService } from 'src/engine/metadata-modules/skill/skill.service'; -import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; +import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor'; @UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI)) @UseInterceptors( - WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor, + WorkspaceMigrationGraphqlApiExceptionInterceptor, SkillGraphqlApiExceptionInterceptor, ) @Resolver(() => SkillDTO) diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index aa752dd59c..9a16e2be34 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -225,6 +225,7 @@ describe('WorkspaceEntityManager', () => { IS_SSE_DB_EVENTS_ENABLED: false, IS_COMMAND_MENU_ITEM_ENABLED: false, IS_FILES_FIELD_ENABLED: false, + IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false, }, userWorkspaceRoleMap: {}, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts similarity index 70% rename from packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts index fc8f16a2db..98bdf55657 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor.ts @@ -12,9 +12,11 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { workspaceMigrationBuilderExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-exception-formatter'; +import { workspaceMigrationRunnerExceptionFormatter } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter'; +import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; @Injectable() -export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor +export class WorkspaceMigrationGraphqlApiExceptionInterceptor implements NestInterceptor { constructor(private readonly i18nService: I18nService) {} @@ -31,6 +33,10 @@ export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor workspaceMigrationBuilderExceptionFormatter(error, i18n); } + if (error instanceof WorkspaceMigrationRunnerException) { + workspaceMigrationRunnerExceptionFormatter(error); + } + throw error; }), ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter.ts new file mode 100644 index 0000000000..52915fa1b1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-runner-exception-formatter.ts @@ -0,0 +1,47 @@ +import { + BaseGraphQLError, + ErrorCode, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + type WorkspaceMigrationRunnerException, + WorkspaceMigrationRunnerExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + +export const workspaceMigrationRunnerExceptionFormatter = ( + error: WorkspaceMigrationRunnerException, +) => { + const isExecutionFailed = + error.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED; + + throw new BaseGraphQLError( + error.message, + isExecutionFailed + ? ErrorCode.APPLICATION_INSTALLATION_FAILED + : ErrorCode.INTERNAL_SERVER_ERROR, + { + code: error.code, + ...(isExecutionFailed && { + action: error.action, + errors: { + ...(error.errors?.metadata && { + metadata: { + message: error.errors.metadata.message, + code: + (error.errors.metadata as { code?: string })?.code ?? + 'INTERNAL_SERVER_ERROR', + }, + }), + ...(error.errors?.workspaceSchema && { + workspaceSchema: { + message: error.errors.workspaceSchema.message, + code: + (error.errors.workspaceSchema as { code?: string })?.code ?? + 'INTERNAL_SERVER_ERROR', + }, + }), + }, + }), + userFriendlyMessage: error.userFriendlyMessage, + }, + ); +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts index 27bce3634d..84172205e7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-build-orchestrator.service.ts @@ -1058,13 +1058,6 @@ export class WorkspaceMigrationBuildOrchestratorService { }; } - const relatedFlatEntityMapsKeys = [ - ...new Set([ - ...Object.keys(fromToAllFlatEntityMaps), - ...Object.keys(dependencyAllFlatEntityMaps ?? {}), - ]), - ] as (keyof AllFlatEntityMaps)[]; - const { aggregatedOrchestratorActionsReport } = aggregateOrchestratorActionsReport({ orchestratorActionsReport, @@ -1075,7 +1068,6 @@ export class WorkspaceMigrationBuildOrchestratorService { return { status: 'success', workspaceMigration: { - relatedFlatEntityMapsKeys, actions: [ // Object and fields and indexes ...aggregatedOrchestratorActionsReport.index.delete, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts index 7af9ef6164..a6f61bbdd5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service.ts @@ -196,15 +196,9 @@ export class WorkspaceMigrationValidateBuildAndRunService { return; } - await this.workspaceMigrationRunnerService - .run(validateAndBuildResult.workspaceMigration) - .catch((error) => { - this.logger.error(error); - throw new WorkspaceMigrationV2Exception( - WorkspaceMigrationV2ExceptionCode.RUNNER_INTERNAL_SERVER_ERROR, - error.message, - ); - }); + await this.workspaceMigrationRunnerService.run( + validateAndBuildResult.workspaceMigration, + ); } public async validateBuildAndRunWorkspaceMigration({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant.ts new file mode 100644 index 0000000000..cb1f09daa9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant.ts @@ -0,0 +1,5 @@ +export const WORKSPACE_MIGRATION_ACTION_TYPE = { + delete: 'delete', + create: 'create', + update: 'update', +} as const; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type.ts index 3aeacb66b9..08ef21e92a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-create-workspace-migration-action.type.ts @@ -1,9 +1,10 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; +import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; export type BaseCreateWorkspaceMigrationAction = { flatEntity: MetadataFlatEntity; - type: 'create'; + type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.create; metadataName: T; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type.ts index a4454ef1ed..becaa6cb4d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-delete-workspace-migration-action.type.ts @@ -1,7 +1,9 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; +import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; + export type BaseDeleteWorkspaceMigrationAction = { universalIdentifier: string; - type: 'delete'; + type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.delete; metadataName: T; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type.ts index c9027eb190..1dcf88fd39 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-update-workspace-migration-action.type.ts @@ -1,9 +1,10 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type'; +import { type WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant'; export type BaseUpdateWorkspaceMigrationAction = { - type: 'update'; + type: typeof WORKSPACE_MIGRATION_ACTION_TYPE.update; metadataName: T; entityId: string; updates: FlatEntityPropertiesUpdates; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts index 3b71032927..2bedbf8a3e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration.ts @@ -1,4 +1,3 @@ -import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; export type WorkspaceMigration< @@ -6,6 +5,4 @@ export type WorkspaceMigration< > = { actions: TActions[]; workspaceId: string; - // TODO remove from workspaceMigration once we've refactored the actions to have metadata and action type grain - relatedFlatEntityMapsKeys?: (keyof AllFlatEntityMaps)[]; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts index df18204a83..7176406406 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts @@ -27,9 +27,9 @@ import { isPropertyUpdate } from 'src/engine/workspace-manager/workspace-migrati import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action'; import { serializeDefaultValue } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util'; import { - WorkspaceMigrationRunnerException, - WorkspaceMigrationRunnerExceptionCode, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { fieldMetadataTypeToColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util'; import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; @@ -243,10 +243,10 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct ); if (!isDefined(foreignKeyName)) { - throw new WorkspaceMigrationRunnerException( - 'Foreign key not found', - WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: 'Foreign key not found', + code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED, + }); } await this.workspaceSchemaManagerService.foreignKeyManager.dropForeignKey( @@ -300,10 +300,11 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct for (const property of compositeType.properties) { if (isMorphOrRelationFieldMetadataType(property.type)) { - throw new WorkspaceMigrationRunnerException( - 'Relation field metadata in composite type is not supported yet', - WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: + 'Relation field metadata in composite type is not supported yet', + code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED, + }); } const fromCompositeColumnName = computeCompositeColumnName( @@ -366,10 +367,11 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct ) as ColumnType; if (isMorphOrRelationFieldMetadataType(property.type)) { - throw new WorkspaceMigrationRunnerException( - 'Relation field metadata in composite type is not supported yet', - WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: + 'Relation field metadata in composite type is not supported yet', + code: WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED, + }); } const compositeColumnName = computeCompositeColumnName( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils.ts index de529a1897..fa346f7bd2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils.ts @@ -2,10 +2,6 @@ import { compositeTypeDefinitions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type QueryRunner } from 'typeorm'; -import { - WorkspaceQueryRunnerException, - WorkspaceQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { @@ -24,6 +20,10 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; +import { + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util'; export const computeFlatIndexFieldColumnNames = ({ @@ -104,10 +104,10 @@ export const insertIndexMetadata = async ({ ); if (indexInsertResult.identifiers.length !== 1) { - throw new WorkspaceQueryRunnerException( - 'Failed to create index metadata', - WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: 'Failed to create index metadata', + code: WorkspaceMigrationActionExecutionExceptionCode.INTERNAL_SERVER_ERROR, + }); } const indexMetadataId = indexInsertResult.identifiers[0].id; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts index e5644d30ef..1955160409 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts @@ -67,7 +67,10 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra } async rollbackForMetadata( - context: WorkspaceMigrationActionRunnerArgs, + context: Omit< + WorkspaceMigrationActionRunnerArgs, + 'queryRunner' + >, ): Promise { const { action, allFlatEntityMaps } = context; const { universalIdentifier } = action; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception.ts new file mode 100644 index 0000000000..8222d3eddf --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception.ts @@ -0,0 +1,64 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; +import { assertUnreachable, CustomError } from 'twenty-shared/utils'; + +export const WorkspaceMigrationActionExecutionExceptionCode = { + FIELD_METADATA_NOT_FOUND: 'FIELD_METADATA_NOT_FOUND', + OBJECT_METADATA_NOT_FOUND: 'OBJECT_METADATA_NOT_FOUND', + ENUM_OPERATION_FAILED: 'ENUM_OPERATION_FAILED', + UNSUPPORTED_COMPOSITE_COLUMN_TYPE: 'UNSUPPORTED_COMPOSITE_COLUMN_TYPE', + NOT_SUPPORTED: 'NOT_SUPPORTED', + INVALID_ACTION_TYPE: 'INVALID_ACTION_TYPE', + FLAT_ENTITY_NOT_FOUND: 'FLAT_ENTITY_NOT_FOUND', + UNSUPPORTED_FIELD_METADATA_TYPE: 'UNSUPPORTED_FIELD_METADATA_TYPE', + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', +} as const; + +const getWorkspaceMigrationActionExecutionExceptionUserFriendlyMessage = ( + code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode, +) => { + switch (code) { + case WorkspaceMigrationActionExecutionExceptionCode.FIELD_METADATA_NOT_FOUND: + return msg`Field metadata not found.`; + case WorkspaceMigrationActionExecutionExceptionCode.OBJECT_METADATA_NOT_FOUND: + return msg`Object metadata not found.`; + case WorkspaceMigrationActionExecutionExceptionCode.ENUM_OPERATION_FAILED: + return msg`Enum operation failed.`; + case WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE: + return msg`Unsupported composite column type.`; + case WorkspaceMigrationActionExecutionExceptionCode.NOT_SUPPORTED: + return msg`This operation is not supported.`; + case WorkspaceMigrationActionExecutionExceptionCode.INVALID_ACTION_TYPE: + return msg`Invalid action type.`; + case WorkspaceMigrationActionExecutionExceptionCode.FLAT_ENTITY_NOT_FOUND: + return msg`Entity not found.`; + case WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE: + return msg`Unsupported field metadata type.`; + case WorkspaceMigrationActionExecutionExceptionCode.INTERNAL_SERVER_ERROR: + return msg`An unexpected error occurred.`; + default: + assertUnreachable(code); + } +}; + +export class WorkspaceMigrationActionExecutionException extends CustomError { + code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode; + userFriendlyMessage: MessageDescriptor; + + constructor({ + message, + code, + userFriendlyMessage, + }: { + message: string; + code: keyof typeof WorkspaceMigrationActionExecutionExceptionCode; + userFriendlyMessage?: MessageDescriptor; + }) { + super(message); + + this.code = code; + this.userFriendlyMessage = + userFriendlyMessage ?? + getWorkspaceMigrationActionExecutionExceptionUserFriendlyMessage(code); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts index dc2ad09c36..473e4236d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts @@ -1,62 +1,68 @@ import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; -import { assertUnreachable } from 'twenty-shared/utils'; +import { assertUnreachable, CustomError } from 'twenty-shared/utils'; -import { - appendCommonExceptionCode, - CustomException, -} from 'src/utils/custom-exception'; +import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; -export const WorkspaceMigrationRunnerExceptionCode = appendCommonExceptionCode({ - FIELD_METADATA_NOT_FOUND: 'FIELD_METADATA_NOT_FOUND', - OBJECT_METADATA_NOT_FOUND: 'OBJECT_METADATA_NOT_FOUND', - ENUM_OPERATION_FAILED: 'ENUM_OPERATION_FAILED', - UNSUPPORTED_COMPOSITE_COLUMN_TYPE: 'UNSUPPORTED_COMPOSITE_COLUMN_TYPE', - NOT_SUPPORTED: 'NOT_SUPPORTED', - INVALID_ACTION_TYPE: 'INVALID_ACTION_TYPE', - FLAT_ENTITY_NOT_FOUND: 'FLAT_ENTITY_NOT_FOUND', - UNSUPPORTED_FIELD_METADATA_TYPE: 'UNSUPPORTED_FIELD_METADATA_TYPE', -} as const); +export const WorkspaceMigrationRunnerExceptionCode = { + INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', + EXECUTION_FAILED: 'EXECUTION_FAILED', +} as const; const getWorkspaceMigrationRunnerExceptionUserFriendlyMessage = ( code: keyof typeof WorkspaceMigrationRunnerExceptionCode, ) => { switch (code) { - case WorkspaceMigrationRunnerExceptionCode.FIELD_METADATA_NOT_FOUND: - return msg`Field metadata not found.`; - case WorkspaceMigrationRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND: - return msg`Object metadata not found.`; - case WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED: - return msg`Enum operation failed.`; - case WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE: - return msg`Unsupported composite column type.`; - case WorkspaceMigrationRunnerExceptionCode.NOT_SUPPORTED: - return msg`This operation is not supported.`; - case WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE: - return msg`Invalid action type.`; - case WorkspaceMigrationRunnerExceptionCode.FLAT_ENTITY_NOT_FOUND: - return msg`Entity not found.`; case WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR: return msg`An unexpected error occurred.`; - case WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE: - return msg`Unsupported field metadata type.`; + case WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED: + return msg`Migration execution failed.`; default: assertUnreachable(code); } }; -export class WorkspaceMigrationRunnerException extends CustomException< - keyof typeof WorkspaceMigrationRunnerExceptionCode -> { - constructor( - message: string, - code: keyof typeof WorkspaceMigrationRunnerExceptionCode, - { userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {}, - ) { - super(message, code, { - userFriendlyMessage: - userFriendlyMessage ?? - getWorkspaceMigrationRunnerExceptionUserFriendlyMessage(code), - }); +export type WorkspaceMigrationRunnerExecutionErrors = { + metadata?: Error; + workspaceSchema?: Error; +}; + +type WorkspaceMigrationRunnerExceptionConstructorArgs = + | { + message: string; + code: typeof WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR; + userFriendlyMessage?: MessageDescriptor; + } + | { + action: WorkspaceMigrationAction; + errors: WorkspaceMigrationRunnerExecutionErrors; + code: typeof WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED; + userFriendlyMessage?: MessageDescriptor; + }; + +export class WorkspaceMigrationRunnerException extends CustomError { + code: keyof typeof WorkspaceMigrationRunnerExceptionCode; + userFriendlyMessage: MessageDescriptor; + action?: WorkspaceMigrationAction; + errors?: WorkspaceMigrationRunnerExecutionErrors; + + constructor(args: WorkspaceMigrationRunnerExceptionConstructorArgs) { + if (args.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED) { + super( + `Migration action '${args.action.type}' for '${args.action.metadataName}' failed`, + ); + + this.code = args.code; + this.action = args.action; + this.errors = args.errors; + } else { + super(args.message); + + this.code = args.code; + } + + this.userFriendlyMessage = + args.userFriendlyMessage ?? + getWorkspaceMigrationRunnerExceptionUserFriendlyMessage(args.code); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts index 6cc3673ace..59e1f38fa0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts @@ -13,6 +13,10 @@ import { type WorkspaceMigrationAction, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/workspace-migration-action-handler-metadata-key.constant'; +import { + WorkspaceMigrationRunnerException, + WorkspaceMigrationRunnerExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; import { type WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { optimisticallyApplyCreateActionOnAllFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-create-action-on-all-flat-entity-maps.util'; import { optimisticallyApplyDeleteActionOnAllFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/optimistically-apply-delete-action-on-all-flat-entity-maps.util'; @@ -24,7 +28,6 @@ type OptimisticallyApplyActionOnAllFlatEntityMapsArgs< WorkspaceMigrationActionRunnerArgs, 'allFlatEntityMaps' | 'action' >; - export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< TActionType extends WorkspaceMigrationActionType, TMetadataName extends AllMetadataName, @@ -80,7 +83,7 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< } rollbackForMetadata( - _context: WorkspaceMigrationActionRunnerArgs, + _context: Omit, 'queryRunner'>, ): Promise { return Promise.resolve(); } @@ -94,32 +97,53 @@ export abstract class BaseWorkspaceMigrationRunnerActionHandlerService< | MetadataToFlatEntityMapsKey > > { - try { - await Promise.all([ - this.asyncMethodPerformanceMetricWrapper({ - label: 'executeForMetadata', - method: async () => this.executeForMetadata(context), - }), - this.asyncMethodPerformanceMetricWrapper({ - label: 'executeForWorkspaceSchema', - method: async () => this.executeForWorkspaceSchema(context), - }), - ]); + const [metadataResult, workspaceSchemaResult] = await Promise.allSettled([ + this.asyncMethodPerformanceMetricWrapper({ + label: 'executeForMetadata', + method: async () => this.executeForMetadata(context), + }), + this.asyncMethodPerformanceMetricWrapper({ + label: 'executeForWorkspaceSchema', + method: async () => this.executeForWorkspaceSchema(context), + }), + ]); - return this.optimisticallyApplyActionOnAllFlatEntityMaps({ + const hasMetadataError = metadataResult.status === 'rejected'; + const hasWorkspaceSchemaError = workspaceSchemaResult.status === 'rejected'; + + if (hasMetadataError || hasWorkspaceSchemaError) { + throw new WorkspaceMigrationRunnerException({ + action: context.action, + errors: { + ...(hasMetadataError && { metadata: metadataResult.reason }), + ...(hasWorkspaceSchemaError && { + workspaceSchema: workspaceSchemaResult.reason, + }), + }, + code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED, + }); + } + + const partialOptimisticCache = + this.optimisticallyApplyActionOnAllFlatEntityMaps({ action: context.action, allFlatEntityMaps: context.allFlatEntityMaps, }); - } catch (error) { - this.logger.error(`${this.actionType} execution failed`, error); - throw error; - } + + return partialOptimisticCache; } async rollback( - context: WorkspaceMigrationActionRunnerArgs, + context: Omit, 'queryRunner'>, ): Promise { - await this.rollbackForMetadata(context); + try { + await this.rollbackForMetadata(context); + } catch (error) { + this.logger.error( + `Failed to rollback ${context.action.type} action for ${context.action.metadataName}: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'BaseWorkspaceMigrationRunnerActionHandlerService', + ); + } } private async asyncMethodPerformanceMetricWrapper({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service.ts index 2072fa58a6..d4029c88fe 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service.ts @@ -1,22 +1,19 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { DiscoveryService } from '@nestjs/core'; -import { isDefined } from 'twenty-shared/utils'; - import { BaseWorkspaceMigrationRunnerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; -import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { buildActionHandlerKey, - type WorkspaceMigrationActionHandlerKey, type WorkspaceMigrationAction, + type WorkspaceMigrationActionHandlerKey, } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module'; import { WORKSPACE_MIGRATION_ACTION_HANDLER_METADATA_KEY } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/constants/workspace-migration-action-handler-metadata-key.constant'; import { - WorkspaceMigrationRunnerException, - WorkspaceMigrationRunnerExceptionCode, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; @Injectable() @@ -56,15 +53,7 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService }); } - async executeActionHandler({ - action, - context, - rollback, - }: { - action: T; - context: WorkspaceMigrationActionRunnerArgs; - rollback?: boolean; - }): Promise> { + private getActionHandler(action: T) { const actionHandlerKey = buildActionHandlerKey( action.type, action.metadataName, @@ -72,18 +61,36 @@ export class WorkspaceMigrationRunnerActionHandlerRegistryService const handler = this.actionHandlers.get(actionHandlerKey); if (!handler) { - throw new WorkspaceMigrationRunnerException( - `No migration runner action handler found for action: ${actionHandlerKey}`, - WorkspaceMigrationRunnerExceptionCode.INVALID_ACTION_TYPE, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: `No migration runner action handler found for action: ${actionHandlerKey}`, + code: WorkspaceMigrationActionExecutionExceptionCode.INVALID_ACTION_TYPE, + }); } - if (isDefined(rollback) && rollback) { - await handler.rollback(context); + return handler; + } - return {}; - } + async executeActionHandler({ + action, + context, + }: { + action: T; + context: WorkspaceMigrationActionRunnerArgs; + }) { + const handler = this.getActionHandler(action); return await handler.execute(context); } + + async executeActionRollbackHandler({ + action, + context, + }: { + action: T; + context: Omit, 'queryRunner'>; + }) { + const handler = this.getActionHandler(action); + + await handler.rollback(context); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts index 3d18094f0e..ffddc8e81a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts @@ -1,23 +1,24 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; +import { AllMetadataName } from 'twenty-shared/metadata'; import { DataSource } from 'typeorm'; -import { - WorkspaceQueryRunnerException, - WorkspaceQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; -import { getMetadataNameFromFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-name-from-flat-entity-maps-key.util'; import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration'; +import { WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; +import { + WorkspaceMigrationRunnerException, + WorkspaceMigrationRunnerExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/registry/workspace-migration-runner-action-handler-registry.service'; @Injectable() @@ -104,35 +105,93 @@ export class WorkspaceMigrationRunnerService { return asyncOperations; } + private async invalidateCachePostExecution({ + allFlatEntityMapsKeys, + workspaceId, + actions, + }: { + allFlatEntityMapsKeys: (keyof AllFlatEntityMaps)[]; + workspaceId: string; + actions: WorkspaceMigrationAction[]; + }): Promise { + this.logger.time( + 'Runner', + `Cache invalidation ${allFlatEntityMapsKeys.join()}`, + ); + + await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ + workspaceId, + flatMapsKeys: allFlatEntityMapsKeys, + }); + + const invalidationResults = await Promise.allSettled( + this.getLegacyCacheInvalidationPromises({ + workspaceMigration: { + actions, + workspaceId, + }, + }), + ); + + const invalidationFailures = invalidationResults.filter( + (result) => result.status === 'rejected', + ); + + if (invalidationFailures.length > 0) { + invalidationFailures.forEach((err) => + this.logger.error( + `Failed to invalidate a legacy cache ${err.reason}`, + 'Runner', + ), + ); + throw new Error( + `Failed to invalidate ${invalidationFailures.length} cache operations`, + ); + } + + this.logger.timeEnd( + 'Runner', + `Cache invalidation ${allFlatEntityMapsKeys.join()}`, + ); + } + run = async ({ actions, workspaceId, - relatedFlatEntityMapsKeys, }: WorkspaceMigration): Promise => { this.logger.time('Runner', 'Total execution'); this.logger.time('Runner', 'Initial cache retrieval'); const queryRunner = this.coreDataSource.createQueryRunner(); + const actionMetadataNames = [ + ...new Set(actions.flatMap((action) => action.metadataName)), + ]; + const actionsMetadataAndRelatedMetadataNames: AllMetadataName[] = [ + ...new Set([ + ...actionMetadataNames, + ...actionMetadataNames.flatMap(getMetadataRelatedMetadataNames), + ]), + ]; + const allFlatEntityMapsKeys = actionsMetadataAndRelatedMetadataNames.map( + getMetadataFlatEntityMapsKey, + ); let allFlatEntityMaps = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: relatedFlatEntityMapsKeys, + flatMapsKeys: allFlatEntityMapsKeys, }, ); this.logger.timeEnd('Runner', 'Initial cache retrieval'); this.logger.time('Runner', 'Transaction execution'); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - let flatEntityMapsToInvalidate: (keyof AllFlatEntityMaps)[] = []; - try { + await queryRunner.connect(); + await queryRunner.startTransaction(); for (const action of actions) { - const partialOptimisticCache = + const result = await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler( { action, @@ -144,20 +203,10 @@ export class WorkspaceMigrationRunnerService { }, }, ); - const optimisticallyUpdatedFlatEntityMapsKeys = Object.keys( - partialOptimisticCache, - ) as (keyof AllFlatEntityMaps)[]; - - flatEntityMapsToInvalidate = [ - ...new Set([ - ...optimisticallyUpdatedFlatEntityMapsKeys, - ...flatEntityMapsToInvalidate, - ]), - ]; allFlatEntityMaps = { ...allFlatEntityMaps, - ...partialOptimisticCache, + ...result, }; } @@ -165,89 +214,46 @@ export class WorkspaceMigrationRunnerService { this.logger.timeEnd('Runner', 'Transaction execution'); - const flatEntitiesCacheToInvalidate = [ - ...new Set([ - ...flatEntityMapsToInvalidate, - ...flatEntityMapsToInvalidate - .map(getMetadataNameFromFlatEntityMapsKey) - .flatMap(getMetadataRelatedMetadataNames) - .map(getMetadataFlatEntityMapsKey), - ]), - ]; - - this.logger.time( - 'Runner', - `Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ + await this.invalidateCachePostExecution({ + allFlatEntityMapsKeys, workspaceId, - flatMapsKeys: flatEntitiesCacheToInvalidate, + actions, }); - const invalidationResults = await Promise.allSettled( - this.getLegacyCacheInvalidationPromises({ - workspaceMigration: { - actions, - workspaceId, - }, - }), - ); - - const invalidationFailures = invalidationResults.filter( - (result) => result.status === 'rejected', - ); - - if (invalidationFailures.length > 0) { - invalidationFailures.forEach((err) => - this.logger.error( - `Failed to invalidate a legacy cache ${err.reason}`, - 'Runner', - ), - ); - throw new Error( - `Failed to invalidate ${invalidationFailures.length} cache operations`, - ); - } - - this.logger.timeEnd( - 'Runner', - `Cache invalidation ${flatEntitiesCacheToInvalidate.join()}`, - ); this.logger.timeEnd('Runner', 'Total execution'); return allFlatEntityMaps; } catch (error) { if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { + await queryRunner.rollbackTransaction().catch((error) => // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } + console.trace(`Failed to rollback transaction: ${error.message}`), + ); } - const invertedActions = actions.reverse(); + const invertedActions = [...actions].reverse(); for (const invertedAction of invertedActions) { - await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionHandler( + await this.workspaceMigrationRunnerActionHandlerRegistry.executeActionRollbackHandler( { action: invertedAction, context: { action: invertedAction, - allFlatEntityMaps: allFlatEntityMaps, - queryRunner, + allFlatEntityMaps, workspaceId, }, - rollback: true, }, ); } - throw new WorkspaceQueryRunnerException( - error.message, - WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, - ); + if (error instanceof WorkspaceMigrationRunnerException) { + throw error; + } + + throw new WorkspaceMigrationRunnerException({ + message: error.message, + code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR, + }); } finally { await queryRunner.release(); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/workspace-schema-enum-operations.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/workspace-schema-enum-operations.util.spec.ts index daa3ae5b7e..d6588b4d0b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/workspace-schema-enum-operations.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/workspace-schema-enum-operations.util.spec.ts @@ -3,7 +3,7 @@ import { type QueryRunner } from 'typeorm'; import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; -import { WorkspaceMigrationRunnerException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; +import { WorkspaceMigrationActionExecutionException } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { collectEnumOperationsForField, EnumOperation, @@ -61,7 +61,7 @@ describe('WorkspaceSchemaEnumOperations', () => { schemaName: 'test_schema', workspaceSchemaManagerService: mockSchemaManagerService, }), - ).rejects.toThrow(WorkspaceMigrationRunnerException); + ).rejects.toThrow(WorkspaceMigrationActionExecutionException); // All operations should be attempted in parallel despite failure expect( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util.ts index af0becff15..c4a95eddf5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util.ts @@ -1,9 +1,9 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { - WorkspaceMigrationRunnerException, - WorkspaceMigrationRunnerExceptionCode, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { isTextColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/is-text-column-type.util'; export const fieldMetadataTypeToColumnType = ( @@ -41,9 +41,9 @@ export const fieldMetadataTypeToColumnType = ( case FieldMetadataType.TS_VECTOR: return 'tsvector'; default: - throw new WorkspaceMigrationRunnerException( - `Cannot convert ${fieldMetadataType} to column type.`, - WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: `Cannot convert ${fieldMetadataType} to column type.`, + code: WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_FIELD_METADATA_TYPE, + }); } }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util.ts index 2d754342bc..4d3e58658b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util.ts @@ -16,9 +16,9 @@ import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/work import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration/utils/compute-postgres-enum-name.util'; import { serializeDefaultValue } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util'; import { - WorkspaceMigrationRunnerException, - WorkspaceMigrationRunnerExceptionCode, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; import { fieldMetadataTypeToColumnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/field-metadata-type-to-column-type.util'; import { getWorkspaceSchemaContextForMigration } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/get-workspace-schema-context-for-migration.util'; @@ -40,10 +40,10 @@ export const generateCompositeColumnDefinition = ({ compositeProperty.type === FieldMetadataType.RELATION || compositeProperty.type === FieldMetadataType.MORPH_RELATION ) { - throw new WorkspaceMigrationRunnerException( - `Relation type not supported for composite columns`, - WorkspaceMigrationRunnerExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: `Relation type not supported for composite columns`, + code: WorkspaceMigrationActionExecutionExceptionCode.UNSUPPORTED_COMPOSITE_COLUMN_TYPE, + }); } const columnName = computeCompositeColumnName( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/workspace-schema-enum-operations.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/workspace-schema-enum-operations.util.ts index 318b0e5314..59d3290e11 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/workspace-schema-enum-operations.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/workspace-schema-enum-operations.util.ts @@ -12,9 +12,9 @@ import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field- import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { computePostgresEnumName } from 'src/engine/workspace-manager/workspace-migration/utils/compute-postgres-enum-name.util'; import { - WorkspaceMigrationRunnerException, - WorkspaceMigrationRunnerExceptionCode, -} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + WorkspaceMigrationActionExecutionException, + WorkspaceMigrationActionExecutionExceptionCode, +} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-action-execution.exception'; export interface CreateEnumOperationSpec { operation: EnumOperation.CREATE; @@ -246,9 +246,9 @@ export const executeBatchEnumOperations = async ({ await Promise.all(enumPromises); } catch (error) { - throw new WorkspaceMigrationRunnerException( - `Failed to execute batch enum operations: ${error instanceof Error ? error.message : 'Unknown error'}`, - WorkspaceMigrationRunnerExceptionCode.ENUM_OPERATION_FAILED, - ); + throw new WorkspaceMigrationActionExecutionException({ + message: `Failed to execute batch enum operations: ${error instanceof Error ? error.message : 'Unknown error'}`, + code: WorkspaceMigrationActionExecutionExceptionCode.ENUM_OPERATION_FAILED, + }); } }; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application-feature-flag-disabled.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application-feature-flag-disabled.integration-spec.ts.snap new file mode 100644 index 0000000000..ae79fa0601 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application-feature-flag-disabled.integration-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Install application should fail when feature flag is disabled should fail with forbidden error when feature flag is disabled 1`] = ` +{ + "extensions": { + "code": "BAD_USER_INPUT", + "subCode": "FORBIDDEN", + "userFriendlyMessage": "You do not have permission to perform this action.", + }, + "message": "Application installation from tarball is not enabled", + "name": "UserInputError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap new file mode 100644 index 0000000000..45484c0835 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-install-application.integration-spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Install application should fail when entity does not exist should fail with execution error when deleting non-existent field metadata 1`] = ` +{ + "eventId": Any, + "extensions": { + "action": { + "metadataName": "fieldMetadata", + "type": "delete", + "universalIdentifier": Any, + }, + "code": "APPLICATION_INSTALLATION_FAILED", + "errors": { + "metadata": { + "code": "ENTITY_NOT_FOUND", + "message": "Could not find flat entity with universal identifier 20202020-6110-4547-9fd0-2525257a2c3f", + }, + "workspaceSchema": { + "code": "ENTITY_NOT_FOUND", + "message": "Could not find flat entity in maps", + }, + }, + "exceptionEventId": Any, + "userFriendlyMessage": "Migration execution failed.", + }, + "message": "Migration action 'delete' for 'fieldMetadata' failed", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application-feature-flag-disabled.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application-feature-flag-disabled.integration-spec.ts new file mode 100644 index 0000000000..f3c3065da9 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application-feature-flag-disabled.integration-spec.ts @@ -0,0 +1,23 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util'; + +describe('Install application should fail when feature flag is disabled', () => { + it('should fail with forbidden error when feature flag is disabled', async () => { + const { errors } = await installApplication({ + expectToFail: true, + input: { + workspaceMigration: { + actions: [ + { + type: 'delete', + metadataName: 'fieldMetadata', + universalIdentifier: '20202020-784f-4042-b58f-ae8dbf718f6e', + }, + ], + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts new file mode 100644 index 0000000000..383549f86e --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-install-application.integration-spec.ts @@ -0,0 +1,44 @@ +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { installApplication } from 'test/integration/metadata/suites/application/utils/install-application.util'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +describe('Install application should fail when entity does not exist', () => { + beforeAll(async () => { + await updateFeatureFlag({ + featureFlag: + FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED, + value: true, + expectToFail: false, + }); + }); + + afterAll(async () => { + await updateFeatureFlag({ + featureFlag: + FeatureFlagKey.IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED, + value: false, + expectToFail: false, + }); + }); + + it('should fail with execution error when deleting non-existent field metadata', async () => { + const { errors } = await installApplication({ + expectToFail: true, + input: { + workspaceMigration: { + actions: [ + { + type: 'delete', + metadataName: 'fieldMetadata', + universalIdentifier: '20202020-6110-4547-9fd0-2525257a2c3f', + }, + ], + }, + }, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application-query-factory.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application-query-factory.util.ts new file mode 100644 index 0000000000..6bfcc719dd --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application-query-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +export type InstallApplicationFactoryInput = { + workspaceMigration: { + actions: { + type: 'delete'; + metadataName: string; + universalIdentifier: string; + }[]; + }; +}; + +export const installApplicationQueryFactory = ({ + input, +}: { + input: InstallApplicationFactoryInput; +}) => ({ + query: gql` + mutation InstallApplication($workspaceMigration: WorkspaceMigrationInput!) { + installApplication(workspaceMigration: $workspaceMigration) + } + `, + variables: { + workspaceMigration: input.workspaceMigration, + }, +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application.util.ts b/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application.util.ts new file mode 100644 index 0000000000..ee9bf566ad --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/utils/install-application.util.ts @@ -0,0 +1,40 @@ +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { + type InstallApplicationFactoryInput, + installApplicationQueryFactory, +} from 'test/integration/metadata/suites/application/utils/install-application-query-factory.util'; +import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; +import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; +import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; + +export const installApplication = async ({ + input, + expectToFail = false, + token, +}: { + input: InstallApplicationFactoryInput; + expectToFail?: boolean; + token?: string; +}): CommonResponseBody<{ + installApplication: boolean; +}> => { + const graphqlOperation = installApplicationQueryFactory({ input }); + + const response = await makeGraphqlAPIRequest(graphqlOperation, token); + + if (expectToFail === true) { + warnIfNoErrorButExpectedToFail({ + response, + errorMessage: 'Install application should have failed but did not', + }); + } + + if (expectToFail === false) { + warnIfErrorButNotExpectedToFail({ + response, + errorMessage: 'Install application has failed but should not', + }); + } + + return { data: response.body.data, errors: response.body.errors }; +};