diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts index 22f730243f..0b14a7a877 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts @@ -37,6 +37,8 @@ import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/co import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; describe('WorkspaceService', () => { @@ -123,7 +125,9 @@ describe('WorkspaceService', () => { PermissionsService, FileCorePictureService, AiModelRegistryService, + ApplicationService, PrefillLogicFunctionService, + WorkspaceMigrationValidateBuildAndRunService, UpgradeMigrationService, UpgradeSequenceReaderService, ].map((service) => ({ diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index b6ad7714df..3ac4fdfb4c 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -12,6 +12,7 @@ import { DataSource, QueryRunner, Repository } from 'typeorm'; import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; @@ -66,6 +67,7 @@ import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/st import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @Injectable() // oxlint-disable-next-line twenty/inject-workspace-repository @@ -116,6 +118,8 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly dnsManagerService: DnsManagerService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly prefillLogicFunctionService: PrefillLogicFunctionService, + private readonly applicationService: ApplicationService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly subdomainManagerService: SubdomainManagerService, private readonly workspaceDataSourceService: WorkspaceDataSourceService, @@ -775,8 +779,6 @@ export class WorkspaceService extends TypeOrmQueryService { flatFieldMetadataMaps, ); - await prefillWorkflowCommandMenuItems(queryRunner.manager, workspaceId); - await prefillOpportunities(queryRunner.manager, schemaName); await prefillDashboards( @@ -788,18 +790,28 @@ export class WorkspaceService extends TypeOrmQueryService { await queryRunner.commitTransaction(); } catch (error) { if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (rollbackError) { - this.logger.error( - `Failed to rollback prefill transaction: ${rollbackError.message}`, - ); - } + await queryRunner.rollbackTransaction(); } throw error; } finally { await queryRunner.release(); } + + try { + await prefillWorkflowCommandMenuItems({ + workspaceId, + applicationService: this.applicationService, + flatEntityMapsCacheService: this.flatEntityMapsCacheService, + workspaceMigrationValidateBuildAndRunService: + this.workspaceMigrationValidateBuildAndRunService, + }); + } catch (error) { + this.logger.error( + `Non-critical: failed to prefill workflow command menu items for workspace ${workspaceId}`, + error, + ); + this.exceptionHandlerService.captureExceptions([error as Error]); + } } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 0a10f588ce..33263e7591 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -43,6 +43,7 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; +import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module'; @Module({ @@ -83,6 +84,7 @@ import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/stand ApplicationModule, EnterpriseModule, StandardObjectsPrefillModule, + WorkspaceMigrationModule, CoreEntityCacheModule, UpgradeModule, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index 94ed4feb33..c7b118c40c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -8,6 +8,7 @@ import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; import { FeatureFlagKey, FileFolder } from 'twenty-shared/types'; import { DataSource } from 'typeorm'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @@ -36,6 +37,10 @@ import { COMPANY_DATA_SEED_COLUMNS, COMPANY_DATA_SEEDS, } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant'; +import { + CONNECTED_ACCOUNT_DATA_SEED_COLUMNS, + CONNECTED_ACCOUNT_DATA_SEEDS, +} from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; import { DASHBOARD_DATA_SEED_COLUMNS, getDashboardDataSeeds, @@ -44,10 +49,6 @@ import { EMPLOYMENT_HISTORY_DATA_SEED_COLUMNS, EMPLOYMENT_HISTORY_DATA_SEEDS, } from 'src/engine/workspace-manager/dev-seeder/data/constants/employment-history-data-seeds.constant'; -import { - CONNECTED_ACCOUNT_DATA_SEED_COLUMNS, - CONNECTED_ACCOUNT_DATA_SEEDS, -} from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; import { MESSAGE_CHANNEL_DATA_SEED_COLUMNS, MESSAGE_CHANNEL_DATA_SEEDS, @@ -121,6 +122,7 @@ import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } f import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util'; import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; type RecordSeedConfig = { tableName: string; @@ -293,6 +295,8 @@ export class DevSeederDataService { private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly prefillLogicFunctionService: PrefillLogicFunctionService, private readonly prefillFrontComponentService: PrefillFrontComponentService, + private readonly applicationService: ApplicationService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, ) {} public async seed({ @@ -366,12 +370,24 @@ export class DevSeederDataService { flatObjectMetadataMaps, flatFieldMetadataMaps, ); - - await prefillWorkflowCommandMenuItems(entityManager, workspaceId); - - await prefillFrontComponentCommandMenuItems(entityManager, workspaceId); }, ); + + await prefillWorkflowCommandMenuItems({ + workspaceId, + applicationService: this.applicationService, + flatEntityMapsCacheService: this.flatEntityMapsCacheService, + workspaceMigrationValidateBuildAndRunService: + this.workspaceMigrationValidateBuildAndRunService, + }); + + await prefillFrontComponentCommandMenuItems({ + workspaceId, + applicationService: this.applicationService, + flatEntityMapsCacheService: this.flatEntityMapsCacheService, + workspaceMigrationValidateBuildAndRunService: + this.workspaceMigrationValidateBuildAndRunService, + }); } private async seedRecordsInBatches({ diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-command-menu-items.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-command-menu-items.util.ts index 421c80a4aa..344a3fb388 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-command-menu-items.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-command-menu-items.util.ts @@ -1,60 +1,82 @@ -import { type EntityManager } from 'typeorm'; +import { v4 } from 'uuid'; +import { isDefined } from 'twenty-shared/utils'; +import { type ApplicationService } from 'src/engine/core-modules/application/application.service'; import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum'; import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { getSeedFrontComponentCommandMenuItemDefinitions, getSeedFrontComponentIds, } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-front-component-definitions.util'; +import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; -export const prefillFrontComponentCommandMenuItems = async ( - entityManager: EntityManager, - workspaceId: string, -) => { +export const prefillFrontComponentCommandMenuItems = async ({ + workspaceId, + applicationService, + flatEntityMapsCacheService, + workspaceMigrationValidateBuildAndRunService, +}: { + workspaceId: string; + applicationService: ApplicationService; + flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService; + workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService; +}): Promise => { const { helloWorldId } = getSeedFrontComponentIds(workspaceId); - const frontComponentRow = await entityManager - .createQueryBuilder() - .select('fc.applicationId', 'applicationId') - .from('core.frontComponent', 'fc') - .where('fc.id = :id', { id: helloWorldId }) - .andWhere('fc.workspaceId = :workspaceId', { workspaceId }) - .getRawOne(); + const { flatCommandMenuItemMaps, flatFrontComponentMaps } = + await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({ + workspaceId, + flatMapsKeys: ['flatCommandMenuItemMaps', 'flatFrontComponentMaps'], + }); - if (!frontComponentRow) { + const frontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: helloWorldId, + flatEntityMaps: flatFrontComponentMaps, + }); + + if (!isDefined(frontComponent)) { return; } + const { workspaceCustomFlatApplication } = + await applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + const definitions = getSeedFrontComponentCommandMenuItemDefinitions(workspaceId); - await entityManager - .createQueryBuilder() - .insert() - .into('core.commandMenuItem', [ - 'workspaceId', - 'universalIdentifier', - 'applicationId', - 'workflowVersionId', - 'frontComponentId', - 'engineComponentKey', - 'label', - 'icon', - 'shortLabel', - 'position', - 'isPinned', - 'availabilityType', - 'conditionalAvailabilityExpression', - 'availabilityObjectMetadataId', - 'hotKeys', - ]) - .values( - definitions.map((definition) => ({ - workspaceId, + const now = new Date().toISOString(); + + const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = definitions + .filter( + (definition) => + !isDefined( + flatCommandMenuItemMaps.byUniversalIdentifier[ + definition.universalIdentifier + ], + ), + ) + .map((definition) => { + const definitionFrontComponent = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: definition.frontComponentId, + flatEntityMaps: flatFrontComponentMaps, + }); + + return { + id: v4(), universalIdentifier: definition.universalIdentifier, - applicationId: frontComponentRow.applicationId, + applicationId: frontComponent.applicationId, + applicationUniversalIdentifier: + frontComponent.applicationUniversalIdentifier, + workspaceId, workflowVersionId: null, frontComponentId: definition.frontComponentId, + frontComponentUniversalIdentifier: + definitionFrontComponent?.universalIdentifier ?? null, engineComponentKey: EngineComponentKey.FRONT_COMPONENT_RENDERER, label: definition.label, icon: definition.icon, @@ -64,9 +86,37 @@ export const prefillFrontComponentCommandMenuItems = async ( availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: null, availabilityObjectMetadataId: null, + availabilityObjectMetadataUniversalIdentifier: null, + payload: null, hotKeys: null, - })), - ) - .orIgnore() - .execute(); + createdAt: now, + updatedAt: now, + }; + }); + + if (flatCommandMenuItemsToCreate.length === 0) { + return; + } + + const result = + await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: flatCommandMenuItemsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (result.status === 'fail') { + throw new Error( + `Failed to create front component command menu items for workspace ${workspaceId}: ${JSON.stringify(result, null, 2)}`, + ); + } }; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts index 19b95706bc..740f7616db 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts @@ -1,70 +1,96 @@ -import { type EntityManager } from 'typeorm'; +import { v4 } from 'uuid'; +import { isDefined } from 'twenty-shared/utils'; +import { type ApplicationService } from 'src/engine/core-modules/application/application.service'; import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum'; import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; +import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; -import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; +import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER = - 'a1b2c3d4-e5f6-7890-abcd-1234567890ab'; + '5b389a80-345f-42b5-83fa-2e6b6ad95f01'; -export const prefillWorkflowCommandMenuItems = async ( - entityManager: EntityManager, - workspaceId: string, -) => { - const applicationRow = await entityManager - .createQueryBuilder() - .select('app.id') - .from('core.application', 'app') - .where('app.universalIdentifier = :universalIdentifier', { - universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier, - }) - .andWhere('app.workspaceId = :workspaceId', { workspaceId }) - .getRawOne(); +export const prefillWorkflowCommandMenuItems = async ({ + workspaceId, + applicationService, + flatEntityMapsCacheService, + workspaceMigrationValidateBuildAndRunService, +}: { + workspaceId: string; + applicationService: ApplicationService; + flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService; + workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService; +}): Promise => { + const { workspaceCustomFlatApplication } = + await applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); - if (!applicationRow) { + const { flatCommandMenuItemMaps } = + await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({ + workspaceId, + flatMapsKeys: ['flatCommandMenuItemMaps'], + }); + + const alreadyExists = isDefined( + flatCommandMenuItemMaps.byUniversalIdentifier[ + QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER + ], + ); + + if (alreadyExists) { return; } - await entityManager - .createQueryBuilder() - .insert() - .into('core.commandMenuItem', [ - 'workspaceId', - 'universalIdentifier', - 'applicationId', - 'workflowVersionId', - 'frontComponentId', - 'engineComponentKey', - 'label', - 'icon', - 'shortLabel', - 'position', - 'isPinned', - 'availabilityType', - 'conditionalAvailabilityExpression', - 'availabilityObjectMetadataId', - 'hotKeys', - ]) - .values([ + const now = new Date().toISOString(); + + const quickLeadFlatCommandMenuItem: FlatCommandMenuItem = { + id: v4(), + universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER, + applicationId: workspaceCustomFlatApplication.id, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + workspaceId, + workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID, + frontComponentId: null, + frontComponentUniversalIdentifier: null, + engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, + label: 'Quick Lead', + icon: 'IconUserPlus', + shortLabel: 'Quick Lead', + position: 100, + isPinned: false, + availabilityType: CommandMenuItemAvailabilityType.GLOBAL, + conditionalAvailabilityExpression: null, + availabilityObjectMetadataId: null, + availabilityObjectMetadataUniversalIdentifier: null, + payload: null, + hotKeys: null, + createdAt: now, + updatedAt: now, + }; + + const result = + await workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: [quickLeadFlatCommandMenuItem], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, workspaceId, - universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER, - applicationId: applicationRow.app_id, - workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID, - frontComponentId: null, - engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, - label: 'Quick Lead', - icon: 'IconUserPlus', - shortLabel: 'Quick Lead', - position: 100, - isPinned: false, - availabilityType: CommandMenuItemAvailabilityType.GLOBAL, - conditionalAvailabilityExpression: null, - availabilityObjectMetadataId: null, - hotKeys: null, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, }, - ]) - .orIgnore() - .execute(); + ); + + if (result.status === 'fail') { + throw new Error( + `Failed to create Quick Lead command menu item for workspace ${workspaceId}: ${JSON.stringify(result, null, 2)}`, + ); + } };