From db526778e3fa0603b48b7d981460abf49139b9f6 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Fri, 14 Feb 2025 16:49:44 +0100
Subject: [PATCH] Update suspended cleaning command (#10195)
closes https://github.com/twentyhq/core-team-issues/issues/382
---
.../clean-suspended-workspace.email.tsx | 6 +-
packages/twenty-server/.env.example | 5 +-
.../environment/environment-variables.ts | 21 ++-
.../user-workspace/user-workspace.entity.ts | 3 +-
.../services/workspace.service.spec.ts | 9 +-
.../workspace/services/workspace.service.ts | 70 +++++--
.../workspace/workspace.module.ts | 2 +
.../object-metadata.service.ts | 3 -
.../clean-suspended-workspaces.command.ts | 62 ++++--
.../crons/clean-suspended-workspaces.job.ts | 1 +
.../services/cleaner.workspace-service.ts | 177 ++++++++++++------
.../content/developers/self-hosting/setup.mdx | 3 +-
12 files changed, 256 insertions(+), 106 deletions(-)
diff --git a/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx
index 5a5581266..b2e559f16 100644
--- a/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx
+++ b/packages/twenty-emails/src/emails/clean-suspended-workspace.email.tsx
@@ -5,13 +5,13 @@ import { MainText } from 'src/components/MainText';
import { Title } from 'src/components/Title';
type CleanSuspendedWorkspaceEmailProps = {
- inactiveDaysBeforeDelete: number;
+ daysSinceInactive: number;
userName: string;
workspaceDisplayName: string | undefined;
};
export const CleanSuspendedWorkspaceEmail = ({
- inactiveDaysBeforeDelete,
+ daysSinceInactive,
userName,
workspaceDisplayName,
}: CleanSuspendedWorkspaceEmailProps) => {
@@ -26,7 +26,7 @@ export const CleanSuspendedWorkspaceEmail = ({
Your workspace {workspaceDisplayName} has been deleted as your
- subscription expired {inactiveDaysBeforeDelete} days ago.
+ subscription expired {daysSinceInactive} days ago.
diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example
index 042a44b99..c02820462 100644
--- a/packages/twenty-server/.env.example
+++ b/packages/twenty-server/.env.example
@@ -46,8 +46,9 @@ FRONTEND_URL=http://localhost:3001
# SENTRY_FRONT_DSN=https://xxx@xxx.ingest.sentry.io/xxx
# LOG_LEVELS=error,warn
# SERVER_URL=http://localhost:3000
-# WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=30
-# WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION=60
+# WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=7
+# WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION=14
+# WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION=21
# Email Server Settings, see this doc for more info: https://docs.twenty.com/start/self-hosting/#email
# IS_EMAIL_VERIFICATION_REQUIRED=false
# EMAIL_VERIFICATION_TOKEN_EXPIRES_IN=1h
diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts
index c61948da0..709d8bfde 100644
--- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts
+++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts
@@ -877,13 +877,23 @@ export class EnvironmentVariables {
})
@CastToPositiveNumber()
@IsNumber()
- @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
+ @IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION', {
+ message:
+ '"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower than "WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION"',
+ })
+ WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 7;
+
+ @EnvironmentVariablesMetadata({
+ group: EnvironmentVariablesGroup.Other,
+ description: 'Number of inactive days before soft deleting workspaces',
+ })
+ @CastToPositiveNumber()
+ @IsNumber()
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', {
message:
- '"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower than "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"',
+ '"WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION" should be strictly lower than "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"',
})
- @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
- WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 7;
+ WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION = 14;
@EnvironmentVariablesMetadata({
group: EnvironmentVariablesGroup.Other,
@@ -891,8 +901,7 @@ export class EnvironmentVariables {
})
@CastToPositiveNumber()
@IsNumber()
- @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION > 0)
- WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 14;
+ WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 21;
@EnvironmentVariablesMetadata({
group: EnvironmentVariablesGroup.Other,
diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts
index d26834adf..eca6227d6 100644
--- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts
+++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts
@@ -5,6 +5,7 @@ import { SettingsFeatures } from 'twenty-shared';
import {
Column,
CreateDateColumn,
+ DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
@@ -63,7 +64,7 @@ export class UserWorkspace {
updatedAt: Date;
@Field({ nullable: true })
- @Column({ nullable: true, type: 'timestamptz' })
+ @DeleteDateColumn({ type: 'timestamptz' })
deletedAt: Date;
@OneToMany(
diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts
index 3f9cd2064..f4275e181 100644
--- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts
@@ -3,9 +3,11 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
+import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
+import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -15,9 +17,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
+import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
-import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
-import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
import { WorkspaceService } from './workspace.service';
@@ -96,6 +97,10 @@ describe('WorkspaceService', () => {
provide: PermissionsService,
useValue: {},
},
+ {
+ provide: WorkspaceCacheStorageService,
+ useValue: {},
+ },
],
}).compile();
diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
index 2f7ee9032..b96ca2ca0 100644
--- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
@@ -14,6 +14,7 @@ import { Repository } from 'typeorm';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
+import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
@@ -35,9 +36,9 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
+import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
-import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@@ -61,6 +62,7 @@ export class WorkspaceService extends TypeOrmQueryService {
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly permissionsService: PermissionsService,
private readonly customDomainService: CustomDomainService,
+ private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {
super(workspaceRepository);
}
@@ -259,43 +261,71 @@ export class WorkspaceService extends TypeOrmQueryService {
});
}
- async softDeleteWorkspace(id: string) {
- const workspace = await this.workspaceRepository.findOneBy({ id });
-
- assert(workspace, 'Workspace not found');
-
- await this.userWorkspaceRepository.delete({ workspaceId: id });
+ async deleteMetadataSchemaCacheAndUserWorkspace(workspace: Workspace) {
+ await this.userWorkspaceRepository.delete({ workspaceId: workspace.id });
if (this.billingService.isBillingEnabled()) {
await this.billingSubscriptionService.deleteSubscriptions(workspace.id);
}
- await this.workspaceManagerService.delete(id);
+ await this.workspaceManagerService.delete(workspace.id);
return workspace;
}
- async deleteWorkspace(id: string) {
- const userWorkspaces = await this.userWorkspaceRepository.findBy({
- workspaceId: id,
+ async deleteWorkspace(id: string, softDelete = false) {
+ const workspace = await this.workspaceRepository.findOne({
+ where: { id },
+ withDeleted: true,
});
- const workspace = await this.softDeleteWorkspace(id);
+ assert(workspace, 'Workspace not found');
+
+ const userWorkspaces = await this.userWorkspaceRepository.find({
+ where: {
+ workspaceId: id,
+ },
+ withDeleted: true,
+ });
for (const userWorkspace of userWorkspaces) {
- await this.handleRemoveWorkspaceMember(id, userWorkspace.userId);
+ await this.handleRemoveWorkspaceMember(
+ id,
+ userWorkspace.userId,
+ softDelete,
+ );
}
- await this.workspaceRepository.delete(id);
+ await this.workspaceCacheStorageService.flush(
+ workspace.id,
+ workspace.metadataVersion,
+ );
- return workspace;
+ if (softDelete) {
+ return await this.workspaceRepository.softDelete({ id });
+ }
+
+ await this.deleteMetadataSchemaCacheAndUserWorkspace(workspace);
+
+ return await this.workspaceRepository.delete(id);
}
- async handleRemoveWorkspaceMember(workspaceId: string, userId: string) {
- await this.userWorkspaceRepository.delete({
- userId,
- workspaceId,
- });
+ async handleRemoveWorkspaceMember(
+ workspaceId: string,
+ userId: string,
+ softDelete = false,
+ ) {
+ if (softDelete) {
+ await this.userWorkspaceRepository.softDelete({
+ userId,
+ workspaceId,
+ });
+ } else {
+ await this.userWorkspaceRepository.delete({
+ userId,
+ workspaceId,
+ });
+ }
const userWorkspaces = await this.userWorkspaceRepository.find({
where: {
diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
index f6bdda9d2..00759cb5d 100644
--- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts
@@ -21,6 +21,7 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
+import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
@@ -51,6 +52,7 @@ import { WorkspaceService } from './services/workspace.service';
OnboardingModule,
TypeORMModule,
PermissionsModule,
+ WorkspaceCacheStorageModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,
diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts
index 881b9f51a..4005e63c8 100644
--- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts
@@ -411,9 +411,6 @@ export class ObjectMetadataService extends TypeOrmQueryService,
) {
- super(workspaceRepository);
+ super();
}
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: BaseCommandOptions,
- workspaceIds: string[],
+ @Option({
+ flags: '-w, --workspace-id [workspace_id]',
+ description:
+ 'workspace id. Command runs on all suspended workspaces if not provided',
+ required: false,
+ })
+ parseWorkspaceId(val: string): string[] {
+ this.workspaceIds.push(val);
+
+ return this.workspaceIds;
+ }
+
+ async fetchSuspendedWorkspaceIds(): Promise {
+ const suspendedWorkspaces = await this.workspaceRepository.find({
+ select: ['id'],
+ where: {
+ activationStatus: In([WorkspaceActivationStatus.SUSPENDED]),
+ },
+ withDeleted: true,
+ });
+
+ return suspendedWorkspaces.map((workspace) => workspace.id);
+ }
+
+ override async executeBaseCommand(
+ _passedParams: string[],
+ options: BaseCommandOptions,
): Promise {
+ const { dryRun } = options;
+
+ const suspendedWorkspaceIds =
+ this.workspaceIds.length > 0
+ ? this.workspaceIds
+ : await this.fetchSuspendedWorkspaceIds();
+
+ this.logger.log(
+ `${dryRun ? 'DRY RUN - ' : ''}Cleaning ${suspendedWorkspaceIds.length} suspended workspaces`,
+ );
+
await this.cleanerWorkspaceService.batchWarnOrCleanSuspendedWorkspaces(
- workspaceIds,
+ suspendedWorkspaceIds,
+ dryRun,
);
}
}
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
index 8a9da6e18..f5a0d45dd 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts
@@ -24,6 +24,7 @@ export class CleanSuspendedWorkspacesJob {
where: {
activationStatus: WorkspaceActivationStatus.SUSPENDED,
},
+ withDeleted: true,
});
await this.cleanerWorkspaceService.batchWarnOrCleanSuspendedWorkspaces(
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 64879aaa5..7cc543299 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
@@ -2,11 +2,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { render } from '@react-email/render';
+import { differenceInDays } from 'date-fns';
import {
CleanSuspendedWorkspaceEmail,
WarnSuspendedWorkspaceEmail,
} from 'twenty-emails';
-import { WorkspaceActivationStatus } from 'twenty-shared';
+import { isDefined, WorkspaceActivationStatus } from 'twenty-shared';
import { In, Repository } from 'typeorm';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@@ -23,11 +24,10 @@ import {
} from 'src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
-const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24;
-
@Injectable()
export class CleanerWorkspaceService {
private readonly logger = new Logger(CleanerWorkspaceService.name);
+ private readonly inactiveDaysBeforeSoftDelete: number;
private readonly inactiveDaysBeforeDelete: number;
private readonly inactiveDaysBeforeWarn: number;
private readonly maxNumberOfWorkspacesDeletedPerExecution: number;
@@ -43,6 +43,9 @@ export class CleanerWorkspaceService {
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository,
) {
+ this.inactiveDaysBeforeSoftDelete = this.environmentService.get(
+ 'WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION',
+ );
this.inactiveDaysBeforeDelete = this.environmentService.get(
'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION',
);
@@ -64,9 +67,9 @@ export class CleanerWorkspaceService {
order: { updatedAt: 'DESC' },
});
- const daysSinceBillingInactivity = Math.floor(
- (new Date().getTime() - lastSubscription.updatedAt.getTime()) /
- MILLISECONDS_IN_ONE_DAY,
+ const daysSinceBillingInactivity = differenceInDays(
+ new Date(),
+ lastSubscription.updatedAt,
);
return daysSinceBillingInactivity;
@@ -104,7 +107,7 @@ export class CleanerWorkspaceService {
) {
const emailData = {
daysSinceInactive,
- inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
+ inactiveDaysBeforeDelete: this.inactiveDaysBeforeSoftDelete,
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
workspaceDisplayName: `${workspaceDisplayName}`,
};
@@ -124,7 +127,11 @@ export class CleanerWorkspaceService {
});
}
- async warnWorkspaceMembers(workspace: Workspace, daysSinceInactive: number) {
+ async warnWorkspaceMembers(
+ workspace: Workspace,
+ daysSinceInactive: number,
+ dryRun: boolean,
+ ) {
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);
@@ -136,42 +143,45 @@ export class CleanerWorkspaceService {
if (workspaceMembersWarned) {
this.logger.log(
- `Workspace ${workspace.id} ${workspace.displayName} already warned`,
+ `${dryRun ? 'DRY RUN - ' : ''}Workspace ${workspace.id} ${workspace.displayName} already warned`,
);
return;
}
this.logger.log(
- `Sending ${workspace.id} ${
+ `${dryRun ? 'DRY RUN - ' : ''}Sending ${workspace.id} ${
workspace.displayName
} suspended since ${daysSinceInactive} days emails to users ['${workspaceMembers
.map((workspaceUser) => workspaceUser.userId)
.join(', ')}']`,
);
- for (const workspaceMember of workspaceMembers) {
- await this.userVarsService.set({
- userId: workspaceMember.userId,
- workspaceId: workspace.id,
- key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
- value: true,
- });
+ if (!dryRun) {
+ for (const workspaceMember of workspaceMembers) {
+ await this.userVarsService.set({
+ userId: workspaceMember.userId,
+ workspaceId: workspace.id,
+ key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
+ value: true,
+ });
- await this.sendWarningEmail(
- workspaceMember,
- workspace.displayName,
- daysSinceInactive,
- );
+ await this.sendWarningEmail(
+ workspaceMember,
+ workspace.displayName,
+ daysSinceInactive,
+ );
+ }
}
}
async sendCleaningEmail(
workspaceMember: WorkspaceMemberWorkspaceEntity,
workspaceDisplayName: string,
+ daysSinceInactive: number,
) {
const emailData = {
- inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete,
+ daysSinceInactive: daysSinceInactive,
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
workspaceDisplayName,
};
@@ -191,73 +201,126 @@ export class CleanerWorkspaceService {
});
}
- async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) {
+ async informWorkspaceMembersAndSoftDeleteWorkspace(
+ workspace: Workspace,
+ daysSinceInactive: number,
+ dryRun: boolean,
+ ) {
+ if (isDefined(workspace.deletedAt)) {
+ this.logger.log(
+ `${dryRun ? 'DRY RUN - ' : ''}Workspace ${workspace.id} ${
+ workspace.displayName
+ } already soft deleted`,
+ );
+
+ return;
+ }
+
const workspaceMembers =
await this.userService.loadWorkspaceMembers(workspace);
this.logger.log(
- `Sending workspace ${workspace.id} ${
+ `${dryRun ? 'DRY RUN - ' : ''}Sending workspace ${workspace.id} ${
workspace.displayName
} deletion emails to users ['${workspaceMembers
.map((workspaceUser) => workspaceUser.userId)
.join(', ')}']`,
);
- for (const workspaceMember of workspaceMembers) {
- await this.userVarsService.delete({
- userId: workspaceMember.userId,
- workspaceId: workspace.id,
- key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
- });
+ if (!dryRun) {
+ for (const workspaceMember of workspaceMembers) {
+ await this.userVarsService.delete({
+ userId: workspaceMember.userId,
+ workspaceId: workspace.id,
+ key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY,
+ });
- await this.sendCleaningEmail(
- workspaceMember,
- workspace.displayName || '',
- );
+ await this.sendCleaningEmail(
+ workspaceMember,
+ workspace.displayName || '',
+ daysSinceInactive,
+ );
+ }
+
+ await this.workspaceService.deleteWorkspace(workspace.id, true);
}
-
- await this.workspaceService.deleteWorkspace(workspace.id);
this.logger.log(
- `Cleaning Workspace ${workspace.id} ${workspace.displayName}`,
+ `${dryRun ? 'DRY RUN - ' : ''}Soft deleting Workspace ${workspace.id} ${workspace.displayName}`,
);
}
async batchWarnOrCleanSuspendedWorkspaces(
workspaceIds: string[],
+ dryRun = false,
): Promise {
- this.logger.log(`batchWarnOrCleanSuspendedWorkspaces running...`);
+ this.logger.log(
+ `${dryRun ? 'DRY RUN - ' : ''}batchWarnOrCleanSuspendedWorkspaces running...`,
+ );
const workspaces = await this.workspaceRepository.find({
where: {
id: In(workspaceIds),
activationStatus: WorkspaceActivationStatus.SUSPENDED,
},
+ withDeleted: true,
});
let deletedWorkspacesCount = 0;
for (const workspace of workspaces) {
- const workspaceInactivity =
- await this.computeWorkspaceBillingInactivity(workspace);
+ try {
+ const workspaceInactivity =
+ await this.computeWorkspaceBillingInactivity(workspace);
- if (
- workspaceInactivity &&
- workspaceInactivity > this.inactiveDaysBeforeDelete &&
- deletedWorkspacesCount <= this.maxNumberOfWorkspacesDeletedPerExecution
- ) {
- await this.informWorkspaceMembersAndDeleteWorkspace(workspace);
- deletedWorkspacesCount++;
+ const daysSinceSoftDeleted = workspace.deletedAt
+ ? differenceInDays(new Date(), workspace.deletedAt)
+ : 0;
- continue;
- }
- if (
- workspaceInactivity &&
- workspaceInactivity > this.inactiveDaysBeforeWarn &&
- workspaceInactivity <= this.inactiveDaysBeforeDelete
- ) {
- await this.warnWorkspaceMembers(workspace, workspaceInactivity);
+ if (
+ daysSinceSoftDeleted >
+ this.inactiveDaysBeforeDelete - this.inactiveDaysBeforeSoftDelete
+ ) {
+ this.logger.log(
+ `${dryRun ? 'DRY RUN - ' : ''}Destroying workspace ${workspace.id} ${workspace.displayName}`,
+ );
+ if (!dryRun) {
+ await this.workspaceService.deleteWorkspace(workspace.id);
+ }
+
+ continue;
+ }
+ if (
+ workspaceInactivity > this.inactiveDaysBeforeSoftDelete &&
+ deletedWorkspacesCount <=
+ this.maxNumberOfWorkspacesDeletedPerExecution
+ ) {
+ await this.informWorkspaceMembersAndSoftDeleteWorkspace(
+ workspace,
+ workspaceInactivity,
+ dryRun,
+ );
+ deletedWorkspacesCount++;
+
+ continue;
+ }
+ if (
+ workspaceInactivity > this.inactiveDaysBeforeWarn &&
+ workspaceInactivity <= this.inactiveDaysBeforeSoftDelete
+ ) {
+ await this.warnWorkspaceMembers(
+ workspace,
+ workspaceInactivity,
+ dryRun,
+ );
+ }
+ } catch (error) {
+ this.logger.error(
+ `Error while processing workspace ${workspace.id} ${workspace.displayName}: ${error}`,
+ );
}
}
- this.logger.log(`batchWarnOrCleanSuspendedWorkspaces done!`);
+ this.logger.log(
+ `${dryRun ? 'DRY RUN - ' : ''}batchWarnOrCleanSuspendedWorkspaces done!`,
+ );
}
}
diff --git a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx
index b641656e8..5ba1b7be4 100644
--- a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx
+++ b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx
@@ -336,7 +336,8 @@ This feature is WIP and is not yet useful for most users.
### Captcha