Update BackfillCommandMenuItemsCommand with workflow backfill (#18848)
Updated upgrade command to also backfill command menu items for workflows. This has been done in the same command because we need to enable the feature flag once both operations are complete: the creation of the standard command menu items and the creation of workflow command menu items.
This commit is contained in:
+272
-23
@@ -1,25 +1,40 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectDataSource, 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 { Repository } from 'typeorm';
|
||||
import { DataSource, type QueryRunner, Repository } from 'typeorm';
|
||||
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 { 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';
|
||||
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 { 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';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import {
|
||||
WorkflowVersionStatus,
|
||||
type WorkflowVersionWorkspaceEntity,
|
||||
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import {
|
||||
type WorkflowManualTrigger,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-command-menu-items',
|
||||
description:
|
||||
'Backfill missing standard command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
|
||||
'Backfill missing standard and workflow command menu items for existing workspaces and enable IS_COMMAND_MENU_ITEM_ENABLED feature flag',
|
||||
})
|
||||
export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@@ -27,10 +42,13 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
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,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
@@ -42,7 +60,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of missing standard command menu items for workspace ${workspaceId}`,
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isFeatureFlagAlreadyEnabled =
|
||||
@@ -59,6 +77,52 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
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],
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Completed backfill of command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillStandardCommandMenuItems(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
@@ -90,30 +154,21 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
),
|
||||
);
|
||||
|
||||
const numberOfCommandMenuItemsToCreate = commandMenuItemsToCreate.length;
|
||||
|
||||
if (numberOfCommandMenuItemsToCreate === 0) {
|
||||
if (commandMenuItemsToCreate.length === 0) {
|
||||
this.logger.log(
|
||||
`No missing standard command menu items for workspace ${workspaceId}, enabling feature flag only`,
|
||||
`No missing standard command menu items for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${numberOfCommandMenuItemsToCreate} missing standard command menu item(s) for workspace ${workspaceId}`,
|
||||
`Found ${commandMenuItemsToCreate.length} missing standard command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create ${numberOfCommandMenuItemsToCreate} command menu item(s) and enable IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
|
||||
`[DRY RUN] Would create ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -132,6 +187,7 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
queryRunner,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -145,13 +201,206 @@ export class BackfillCommandMenuItemsCommand extends ActiveOrSuspendedWorkspaces
|
||||
);
|
||||
}
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) and enabled IS_COMMAND_MENU_ITEM_ENABLED for workspace ${workspaceId}`,
|
||||
`Successfully backfilled ${commandMenuItemsToCreate.length} standard command menu item(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillWorkflowCommandMenuItems(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
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,
|
||||
);
|
||||
|
||||
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<WorkflowWorkspaceEntity>(
|
||||
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: null,
|
||||
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}`,
|
||||
);
|
||||
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
queryRunner,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to backfill workflow command menu items:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async resolveManualTriggerAvailability(
|
||||
trigger: WorkflowManualTrigger,
|
||||
workspaceId: string,
|
||||
): Promise<{
|
||||
availabilityType: CommandMenuItemAvailabilityType;
|
||||
availabilityObjectMetadataId: string | undefined;
|
||||
}> {
|
||||
const availability = trigger.settings.availability;
|
||||
|
||||
if (!isDefined(availability) || availability.type === 'GLOBAL') {
|
||||
return {
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
availabilityObjectMetadataId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { objectIdByNameSingular } =
|
||||
await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId);
|
||||
|
||||
const objectId = objectIdByNameSingular[availability.objectNameSingular];
|
||||
|
||||
if (!isDefined(objectId)) {
|
||||
this.logger.warn(
|
||||
`Object metadata not found for "${availability.objectNameSingular}" in workspace ${workspaceId}, falling back to GLOBAL`,
|
||||
);
|
||||
|
||||
return {
|
||||
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
|
||||
availabilityObjectMetadataId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
availabilityType: CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
availabilityObjectMetadataId: objectId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -52,6 +53,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
ApplicationRegistrationModule,
|
||||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
providers: [
|
||||
IdentifyPermissionFlagMetadataCommand,
|
||||
|
||||
Reference in New Issue
Block a user