From 13ea3ec75c3bea2c129fbecf22f4b87053cd0a62 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 25 Mar 2026 14:37:58 +0100 Subject: [PATCH] Split command menu items backfill into separate migration runs per application (#18957) ## Summary - The 1-20 backfill command menu items upgrade command was mixing standard and custom application flat entities into a single `validateBuildAndRunWorkspaceMigration` call. Since each migration run is tied to a single application, this split the backfill into two separate runs: standard items under `twentyStandardFlatApplication` and workflow trigger items under `workspaceCustomFlatApplication`. --- ...-20-backfill-command-menu-items.command.ts | 117 +++++++++++------- 1 file changed, 70 insertions(+), 47 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 6b85944681..f1d52df2d6 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 @@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/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'; @@ -76,20 +77,28 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces return; } + const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + const standardCommandMenuItems = - await this.computeStandardCommandMenuItemsToCreate(workspaceId); + await this.computeStandardCommandMenuItemsToCreate( + workspaceId, + twentyStandardFlatApplication, + ); const triggerWorkflowVersionCommandMenuItems = await this.computeTriggerWorkflowVersionCommandMenuItemsToCreate( workspaceId, + workspaceCustomFlatApplication, ); - const allCommandMenuItemsToCreate = [ - ...standardCommandMenuItems, - ...triggerWorkflowVersionCommandMenuItems, - ]; + const totalCount = + standardCommandMenuItems.length + + triggerWorkflowVersionCommandMenuItems.length; - if (allCommandMenuItemsToCreate.length === 0) { + if (totalCount === 0) { this.logger.log( `No missing command menu items for workspace ${workspaceId}`, ); @@ -98,46 +107,33 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces } this.logger.log( - `Found ${allCommandMenuItemsToCreate.length} missing command menu item(s) for workspace ${workspaceId} (${standardCommandMenuItems.length} standard, ${triggerWorkflowVersionCommandMenuItems.length} trigger workflow version)`, + `Found ${totalCount} 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}`, + `[DRY RUN] Would create ${totalCount} command menu item(s) for workspace ${workspaceId}`, ); return; } - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); + if (standardCommandMenuItems.length > 0) { + await this.createCommandMenuItems({ + workspaceId, + flatCommandMenuItemsToCreate: standardCommandMenuItems, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }); + } - 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}`, - ); + if (triggerWorkflowVersionCommandMenuItems.length > 0) { + await this.createCommandMenuItems({ + workspaceId, + flatCommandMenuItemsToCreate: triggerWorkflowVersionCommandMenuItems, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + }); } await this.featureFlagService.enableFeatureFlags( @@ -146,18 +142,14 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces ); this.logger.log( - `Successfully backfilled ${allCommandMenuItemsToCreate.length} command menu item(s) for workspace ${workspaceId}`, + `Successfully backfilled ${totalCount} command menu item(s) for workspace ${workspaceId}`, ); } private async computeStandardCommandMenuItemsToCreate( workspaceId: string, + twentyStandardFlatApplication: FlatApplication, ): Promise { - const { twentyStandardFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - const { allFlatEntityMaps: standardAllFlatEntityMaps } = computeTwentyStandardApplicationAllFlatEntityMaps({ shouldIncludeRecordPageLayouts: true, @@ -195,6 +187,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces private async computeTriggerWorkflowVersionCommandMenuItemsToCreate( workspaceId: string, + workspaceCustomFlatApplication: FlatApplication, ): Promise { const authContext = buildSystemAuthContext(workspaceId); @@ -244,11 +237,6 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces { shouldBypassPermissionChecks: true }, ); - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - const flatCommandMenuItemsToCreate: FlatCommandMenuItem[] = []; for (const workflowVersion of manualTriggerVersions) { @@ -311,6 +299,41 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces ); } + private async createCommandMenuItems({ + workspaceId, + flatCommandMenuItemsToCreate, + applicationUniversalIdentifier, + }: { + workspaceId: string; + flatCommandMenuItemsToCreate: FlatCommandMenuItem[]; + applicationUniversalIdentifier: string; + }): Promise { + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + commandMenuItem: { + flatEntityToCreate: flatCommandMenuItemsToCreate, + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier, + }, + ); + + 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}`, + ); + } + } + private async resolveManualTriggerAvailability( trigger: WorkflowManualTrigger, workspaceId: string,