From 34d126f727e27734ac09b47b28d8fd2f391d2d83 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:51:02 +0200 Subject: [PATCH] add command to destroy workspaces (#13447) Dev tool to enable workspace destroy - two guards added (destroy only billing inactive workspace + soft deleted) closes : https://github.com/twentyhq/core-team-issues/issues/1238 --------- Co-authored-by: Guillim --- .../commands/destroy-workspace.command.ts | 48 +++++++++++++++ .../services/cleaner.workspace-service.ts | 60 +++++++++++++++++++ .../workspace-cleaner.module.ts | 2 + 3 files changed, 110 insertions(+) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command.ts diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command.ts new file mode 100644 index 00000000000..5350f635c0f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command.ts @@ -0,0 +1,48 @@ +import { Command, Option } from 'nest-commander'; + +import { + MigrationCommandOptions, + MigrationCommandRunner, +} from 'src/database/commands/command-runners/migration.command-runner'; +import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service'; + +@Command({ + name: 'workspace:destroy', + description: 'Destroy workspace', +}) +export class DestroyWorkspaceCommand extends MigrationCommandRunner { + private workspaceIds: string[] = []; + + constructor( + private readonly cleanerWorkspaceService: CleanerWorkspaceService, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id ', + description: 'workspace id - mandatory', + required: true, + }) + parseWorkspaceId(val: string): string[] { + this.workspaceIds.push(val); + + return this.workspaceIds; + } + + override async runMigrationCommand( + _passedParams: string[], + options: MigrationCommandOptions, + ): Promise { + const { dryRun } = options; + + this.logger.log( + `${dryRun ? 'DRY RUN - ' : ''}Destroy ${this.workspaceIds.length} workspaces : ${this.workspaceIds.join(', ')}`, + ); + + await this.cleanerWorkspaceService.destroyBillingDeactivatedAndSoftDeletedWorkspaces( + this.workspaceIds, + dryRun, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 9b293257a51..58317705be8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -14,6 +14,7 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { In, Repository } from 'typeorm'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -392,4 +393,63 @@ export class CleanerWorkspaceService { `${dryRun ? 'DRY RUN - ' : ''}batchWarnOrCleanSuspendedWorkspaces done!`, ); } + + async destroyBillingDeactivatedAndSoftDeletedWorkspaces( + workspaceIds: string[], + dryRun = false, + ): Promise { + this.logger.log( + `${dryRun ? 'DRY RUN - ' : ''}destroyBillingDeactivatedAndSoftDeletedWorkspaces running...`, + ); + + const workspaces = await this.workspaceRepository.find({ + where: { + id: In(workspaceIds), + }, + withDeleted: true, + }); + + for (const workspace of workspaces) { + if (!isDefined(workspace.deletedAt)) { + this.logger.warn( + `${dryRun ? 'DRY RUN - ' : ''}Workspace ${workspace.id} is not soft deleted, skipping`, + ); + + continue; + } + + if (this.twentyConfigService.get('IS_BILLING_ENABLED')) { + const activeBillingSubscription = + await this.billingSubscriptionRepository.findOne({ + where: { + workspaceId: workspace.id, + status: In([ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + ]), + }, + }); + + if (isDefined(activeBillingSubscription)) { + this.logger.warn( + `${dryRun ? 'DRY RUN - ' : ''}Workspace ${workspace.id} has an active billing subscription, skipping`, + ); + + continue; + } + } + + this.logger.log( + `${dryRun ? 'DRY RUN - ' : ''}Destroying workspace ${workspace.id}`, + ); + + if (!dryRun) { + await this.workspaceService.deleteWorkspace(workspace.id); + } + + this.logger.log( + `${dryRun ? 'DRY RUN - ' : ''}Destroyed workspace ${workspace.id}`, + ); + } + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts index d51c5125617..7a24cba9888 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts @@ -15,6 +15,7 @@ import { CleanOnboardingWorkspacesCronCommand } from 'src/engine/workspace-manag import { CleanSuspendedWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command'; import { CleanSuspendedWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command'; import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command'; +import { DestroyWorkspaceCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/destroy-workspace.command'; import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service'; @Module({ @@ -33,6 +34,7 @@ import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace- providers: [ DeleteWorkspacesCommand, CleanSuspendedWorkspacesCommand, + DestroyWorkspaceCommand, CleanSuspendedWorkspacesCronCommand, CleanOnboardingWorkspacesCommand, CleanOnboardingWorkspacesCronCommand,