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