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 <guillim@users.noreply.github.com>
This commit is contained in:
Etienne
2025-07-28 16:51:02 +02:00
committed by GitHub
co-authored by Guillim
parent a818a70e40
commit 34d126f727
3 changed files with 110 additions and 0 deletions
@@ -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 <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<void> {
const { dryRun } = options;
this.logger.log(
`${dryRun ? 'DRY RUN - ' : ''}Destroy ${this.workspaceIds.length} workspaces : ${this.workspaceIds.join(', ')}`,
);
await this.cleanerWorkspaceService.destroyBillingDeactivatedAndSoftDeletedWorkspaces(
this.workspaceIds,
dryRun,
);
}
}
@@ -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<void> {
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}`,
);
}
}
}
@@ -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,