From cac92bffe3c8b810ca800c60abc9df2a5795aee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:13:10 +0100 Subject: [PATCH] Refactor backfill-command-menu-items to use a single validateBuildAndRun call (#18923) Refactored `backfill-command-menu-items` upgrade command to remove the manual `QueryRunner` transaction and wrap standard and trigger workflow version command menu item creation into a single `validateBuildAndRunWorkspaceMigration` call. --- ...-20-backfill-command-menu-items.command.ts | 404 ++++++++---------- 1 file changed, 174 insertions(+), 230 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts index 671aed09e0c..6b859446818 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command.ts @@ -1,10 +1,10 @@ -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; import { isNonEmptyString } from '@sniptt/guards'; import { Command } from 'nest-commander'; import { FeatureFlagKey } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { DataSource, type QueryRunner, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { v4 as uuidv4 } from 'uuid'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; @@ -14,8 +14,8 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; 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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @@ -35,7 +35,7 @@ import { @Command({ name: 'upgrade:1-20:backfill-command-menu-items', description: - 'Backfill missing standard and workflow command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag', + 'Backfill missing standard and trigger workflow version command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag', }) export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { constructor( @@ -43,8 +43,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces protected readonly workspaceRepository: Repository, protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - @InjectDataSource() - private readonly coreDataSource: DataSource, private readonly applicationService: ApplicationService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly featureFlagService: FeatureFlagService, @@ -78,52 +76,83 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces return; } - const queryRunner = this.coreDataSource.createQueryRunner(); + const standardCommandMenuItems = + await this.computeStandardCommandMenuItemsToCreate(workspaceId); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - await this.backfillStandardCommandMenuItems( - workspaceId, - isDryRun, - queryRunner, - ); - - await this.backfillWorkflowCommandMenuItems( - workspaceId, - isDryRun, - queryRunner, - ); - - await queryRunner.commitTransaction(); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error( - `Rolling back backfill of command menu items for workspace ${workspaceId}: ${error.message}`, - ); - throw error; - } finally { - await queryRunner.release(); - } - - if (!isDryRun) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED], + const triggerWorkflowVersionCommandMenuItems = + await this.computeTriggerWorkflowVersionCommandMenuItemsToCreate( workspaceId, ); + + const allCommandMenuItemsToCreate = [ + ...standardCommandMenuItems, + ...triggerWorkflowVersionCommandMenuItems, + ]; + + if (allCommandMenuItemsToCreate.length === 0) { + this.logger.log( + `No missing command menu items for workspace ${workspaceId}`, + ); + + return; } this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed backfill of command menu items for workspace ${workspaceId}`, + `Found ${allCommandMenuItemsToCreate.length} missing command menu item(s) for workspace ${workspaceId} (${standardCommandMenuItems.length} standard, ${triggerWorkflowVersionCommandMenuItems.length} trigger workflow version)`, + ); + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would create ${allCommandMenuItemsToCreate.length} command menu item(s) for workspace ${workspaceId}`, + ); + + return; + } + + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: allCommandMenuItemsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to backfill command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + + throw new Error( + `Failed to backfill command menu items for workspace ${workspaceId}`, + ); + } + + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED], + workspaceId, + ); + + this.logger.log( + `Successfully backfilled ${allCommandMenuItemsToCreate.length} command menu item(s) for workspace ${workspaceId}`, ); } - private async backfillStandardCommandMenuItems( + private async computeStandardCommandMenuItemsToCreate( workspaceId: string, - isDryRun: boolean, - queryRunner: QueryRunner, - ): Promise { + ): Promise { const { twentyStandardFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, @@ -155,216 +184,131 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces ), ); - if (commandMenuItemsToCreate.length === 0) { + if (commandMenuItemsToCreate.length > 0) { this.logger.log( - `No missing standard command menu items for workspace ${workspaceId}`, - ); - - return; - } - - this.logger.log( - `Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`, - ); - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would create ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`, - ); - - return; - } - - const validateAndBuildResult = - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - allFlatEntityOperationByMetadataName: { - commandMenuItem: { - flatEntityToCreate: commandMenuItemsToCreate, - flatEntityToDelete: [], - flatEntityToUpdate: [], - }, - }, - workspaceId, - applicationUniversalIdentifier: - twentyStandardFlatApplication.universalIdentifier, - queryRunner, - }, - ); - - if (validateAndBuildResult.status === 'fail') { - this.logger.error( - `Failed to backfill missing standard command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, - ); - - throw new Error( - `Failed to backfill missing standard command menu items for workspace ${workspaceId}`, + `Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`, ); } - this.logger.log( - `Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`, - ); + return commandMenuItemsToCreate; } - private async backfillWorkflowCommandMenuItems( + private async computeTriggerWorkflowVersionCommandMenuItemsToCreate( workspaceId: string, - isDryRun: boolean, - queryRunner: QueryRunner, - ): Promise { + ): Promise { const authContext = buildSystemAuthContext(workspaceId); - await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, + return await this.twentyORMGlobalManager.executeInWorkspaceContext( + async () => { + const workflowVersionRepository = + await this.twentyORMGlobalManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + const activeWorkflowVersions = await workflowVersionRepository.find({ + where: { status: WorkflowVersionStatus.ACTIVE }, + }); + + const manualTriggerVersions = activeWorkflowVersions.filter( + (version) => + isDefined(version.trigger) && + version.trigger.type === WorkflowTriggerType.MANUAL, ); - const activeWorkflowVersions = await workflowVersionRepository.find({ - where: { status: WorkflowVersionStatus.ACTIVE }, - }); + if (manualTriggerVersions.length === 0) { + this.logger.log( + `No active workflow versions with manual triggers for workspace ${workspaceId}`, + ); - const manualTriggerVersions = activeWorkflowVersions.filter( - (version) => - isDefined(version.trigger) && - version.trigger.type === WorkflowTriggerType.MANUAL, - ); - - if (manualTriggerVersions.length === 0) { - this.logger.log( - `No active workflow versions with manual triggers for workspace ${workspaceId}`, - ); - - return; - } - - const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'flatCommandMenuItemMaps', - ]); - - const existingWorkflowVersionIds = new Set( - Object.values(existingFlatCommandMenuItemMaps.byUniversalIdentifier) - .filter(isDefined) - .map((item) => item.workflowVersionId) - .filter(isDefined), - ); - - const workflowRepository = - await this.twentyORMGlobalManager.getRepository( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); - - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - - const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = []; - - for (const workflowVersion of manualTriggerVersions) { - if (existingWorkflowVersionIds.has(workflowVersion.id)) { - continue; + return []; } - const workflow = await workflowRepository.findOne({ - where: { id: workflowVersion.workflowId }, - }); + const { flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatCommandMenuItemMaps', + ]); - const label = - workflow && isNonEmptyString(workflow.name) - ? workflow.name - : 'Manual Trigger'; - - const trigger = workflowVersion.trigger as WorkflowManualTrigger; - - const { availabilityType, availabilityObjectMetadataId } = - await this.resolveManualTriggerAvailability(trigger, workspaceId); - - const id = uuidv4(); - const now = new Date().toISOString(); - - flatCommandMenuItemsToCreate.push({ - id, - universalIdentifier: id, - workflowVersionId: workflowVersion.id, - frontComponentId: null, - frontComponentUniversalIdentifier: null, - engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, - label, - shortLabel: label, - icon: trigger.settings.icon ?? null, - isPinned: trigger.settings.isPinned ?? false, - position: 0, - hotKeys: null, - availabilityType, - availabilityObjectMetadataId: availabilityObjectMetadataId ?? null, - availabilityObjectMetadataUniversalIdentifier: null, - conditionalAvailabilityExpression: null, - workspaceId, - applicationId: workspaceCustomFlatApplication.id, - applicationUniversalIdentifier: - workspaceCustomFlatApplication.universalIdentifier, - createdAt: now, - updatedAt: now, - }); - } - - if (flatCommandMenuItemsToCreate.length === 0) { - this.logger.log( - `No missing workflow command menu items for workspace ${workspaceId}`, + const existingWorkflowVersionIds = new Set( + Object.values(existingFlatCommandMenuItemMaps.byUniversalIdentifier) + .filter(isDefined) + .map((item) => item.workflowVersionId) + .filter(isDefined), ); - return; - } - - this.logger.log( - `Found ${flatCommandMenuItemsToCreate.length} missing workflow command menu item(s) for workspace ${workspaceId}`, - ); - - if (isDryRun) { - this.logger.log( - `[DRY RUN] Would create ${flatCommandMenuItemsToCreate.length} workflow command menu item(s) for workspace ${workspaceId}`, - ); - - return; - } - - const validateAndBuildResult = - await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( - { - allFlatEntityOperationByMetadataName: { - commandMenuItem: { - flatEntityToCreate: flatCommandMenuItemsToCreate, - flatEntityToDelete: [], - flatEntityToUpdate: [], - }, - }, + const workflowRepository = + await this.twentyORMGlobalManager.getRepository( workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); + + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = []; + + for (const workflowVersion of manualTriggerVersions) { + if (existingWorkflowVersionIds.has(workflowVersion.id)) { + continue; + } + + const workflow = await workflowRepository.findOne({ + where: { id: workflowVersion.workflowId }, + }); + + const label = + workflow && isNonEmptyString(workflow.name) + ? workflow.name + : 'Manual Trigger'; + + const trigger = workflowVersion.trigger as WorkflowManualTrigger; + + const { availabilityType, availabilityObjectMetadataId } = + await this.resolveManualTriggerAvailability(trigger, workspaceId); + + const id = uuidv4(); + const now = new Date().toISOString(); + + flatCommandMenuItemsToCreate.push({ + id, + universalIdentifier: id, + workflowVersionId: workflowVersion.id, + frontComponentId: null, + frontComponentUniversalIdentifier: null, + engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, + label, + shortLabel: label, + icon: trigger.settings.icon ?? null, + isPinned: trigger.settings.isPinned ?? false, + position: 0, + hotKeys: null, + availabilityType, + availabilityObjectMetadataId: availabilityObjectMetadataId ?? null, + availabilityObjectMetadataUniversalIdentifier: null, + conditionalAvailabilityExpression: null, + workspaceId, + applicationId: workspaceCustomFlatApplication.id, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, - queryRunner, - }, - ); + createdAt: now, + updatedAt: now, + }); + } - if (validateAndBuildResult.status === 'fail') { - this.logger.error( - `Failed to backfill workflow command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, - ); + if (flatCommandMenuItemsToCreate.length > 0) { + this.logger.log( + `Found ${flatCommandMenuItemsToCreate.length} missing trigger workflow version command menu item(s) for workspace ${workspaceId}`, + ); + } - throw new Error( - `Failed to backfill workflow command menu items for workspace ${workspaceId}`, - ); - } - - this.logger.log( - `Successfully backfilled ${flatCommandMenuItemsToCreate.length} workflow command menu item(s) for workspace ${workspaceId}`, - ); - }, authContext); + return flatCommandMenuItemsToCreate; + }, + authContext, + ); } private async resolveManualTriggerAvailability(