From edd7212f0b039c16794a71e64d8cbbb1b4fe5b07 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Fri, 24 Jan 2025 11:10:52 +0100 Subject: [PATCH] feat: add clean suspended workspaces command (#9808) closes [283 sub-issue](https://github.com/twentyhq/core-team-issues/issues/283) - [parent issue ](https://github.com/orgs/twentyhq/projects/1/views/3?filterQuery=sprint%3A%40current+assignee%3Aetiennejouan&pane=issue&itemId=93520456&issue=twentyhq%7Ccore-team-issues%7C179) --------- Co-authored-by: etiennejouan --- .../core-modules/billing/billing.module.ts | 2 + .../billing-webhook-subscription.service.ts | 65 +++++- .../environment/environment-variables.ts | 9 +- .../core-modules/message-queue/jobs.module.ts | 9 +- ...clean-suspended-workspaces.cron.command.ts | 30 +++ ...pace-deletion-warning-sent-key.constant.ts | 2 + ...clean-suspended-workspaces.cron.pattern.ts | 1 + .../crons/clean-suspended-workspaces.job.ts | 210 ++++++++++++++++++ ...orkspace-deletion-warning-user-vars.job.ts | 74 ++++++ .../workspace-cleaner.module.ts | 8 +- 10 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.cron.pattern.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index ebab7647e..7ccd6142a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -26,6 +26,7 @@ import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billi import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -34,6 +35,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; FeatureFlagModule, StripeModule, DomainManagerModule, + MessageQueueModule, TypeOrmModule.forFeature( [ BillingSubscription, diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts index fcad8e097..854d69b1d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts @@ -13,7 +13,28 @@ import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/se import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util'; import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util'; import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + CleanWorkspaceDeletionWarningUserVarsJob, + CleanWorkspaceDeletionWarningUserVarsJobData, +} from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job'; + +const BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS = { + [WorkspaceActivationStatus.ACTIVE]: [ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + SubscriptionStatus.PastDue, + ], + [WorkspaceActivationStatus.SUSPENDED]: [ + SubscriptionStatus.Canceled, + SubscriptionStatus.Unpaid, + SubscriptionStatus.Paused, + ], +}; + @Injectable() export class BillingWebhookSubscriptionService { protected readonly logger = new Logger( @@ -21,6 +42,8 @@ export class BillingWebhookSubscriptionService { ); constructor( private readonly stripeCustomerService: StripeCustomerService, + @InjectMessageQueue(MessageQueue.workspaceQueue) + private readonly messageQueueService: MessageQueueService, @InjectRepository(BillingSubscription, 'core') private readonly billingSubscriptionRepository: Repository, @InjectRepository(BillingSubscriptionItem, 'core') @@ -62,14 +85,28 @@ export class BillingWebhookSubscriptionService { }, ); - const billingSubscription = - await this.billingSubscriptionRepository.findOneOrFail({ - where: { stripeSubscriptionId: data.object.id }, - }); + const billingSubscriptions = await this.billingSubscriptionRepository.find({ + where: { workspaceId }, + }); + + const updatedBillingSubscription = billingSubscriptions.find( + (subscription) => subscription.stripeSubscriptionId === data.object.id, + ); + + if (!updatedBillingSubscription) { + throw new Error('Billing subscription not found'); + } + + const hasActiveWorkspaceCompatibleSubscription = billingSubscriptions.some( + (subscription) => + BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[ + WorkspaceActivationStatus.ACTIVE + ].includes(subscription.status), + ); await this.billingSubscriptionItemRepository.upsert( transformStripeSubscriptionEventToDatabaseSubscriptionItem( - billingSubscription.id, + updatedBillingSubscription.id, data, ), { @@ -79,9 +116,10 @@ export class BillingWebhookSubscriptionService { ); if ( - data.object.status === SubscriptionStatus.Canceled || - data.object.status === SubscriptionStatus.Unpaid || - data.object.status === SubscriptionStatus.Paused + BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[ + WorkspaceActivationStatus.SUSPENDED + ].includes(data.object.status as SubscriptionStatus) && + !hasActiveWorkspaceCompatibleSubscription ) { await this.workspaceRepository.update(workspaceId, { activationStatus: WorkspaceActivationStatus.SUSPENDED, @@ -89,14 +127,19 @@ export class BillingWebhookSubscriptionService { } if ( - (data.object.status === SubscriptionStatus.Active || - data.object.status === SubscriptionStatus.Trialing || - data.object.status === SubscriptionStatus.PastDue) && + BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[ + WorkspaceActivationStatus.ACTIVE + ].includes(data.object.status as SubscriptionStatus) && workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED ) { await this.workspaceRepository.update(workspaceId, { activationStatus: WorkspaceActivationStatus.ACTIVE, }); + + await this.messageQueueService.add( + CleanWorkspaceDeletionWarningUserVarsJob.name, + { workspaceId }, + ); } await this.stripeCustomerService.updateCustomerMetadataWorkspaceId( 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 04edd54f0..f605ea9f4 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 @@ -371,12 +371,17 @@ export class EnvironmentVariables { '"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower that "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"', }) @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0) - WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 30; + WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 7; @CastToPositiveNumber() @IsNumber() @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION > 0) - WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 60; + WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 14; + + @CastToPositiveNumber() + @IsNumber() + @ValidateIf((env) => env.MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION > 0) + MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION = 5; @IsEnum(CaptchaDriverType) @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts index 1707e74ef..85375708e 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts @@ -7,11 +7,13 @@ import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-w import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job'; import { EmailModule } from 'src/engine/core-modules/email/email.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; +import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -19,6 +21,8 @@ import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.mod import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job'; +import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job'; +import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job'; import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; import { CalendarModule } from 'src/modules/calendar/calendar.module'; import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-creation-manager/jobs/auto-companies-and-contacts-creation-job.module'; @@ -31,11 +35,12 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature([Workspace, BillingSubscription], 'core'), DataSourceModule, ObjectMetadataModule, TypeORMModule, UserModule, + UserVarsModule, EmailModule, DataSeedDemoWorkspaceModule, BillingModule, @@ -55,10 +60,12 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; ], providers: [ CleanInactiveWorkspaceJob, + CleanSuspendedWorkspacesJob, EmailSenderJob, DataSeedDemoWorkspaceJob, UpdateSubscriptionQuantityJob, HandleWorkspaceMemberDeletedJob, + CleanWorkspaceDeletionWarningUserVarsJob, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command.ts new file mode 100644 index 000000000..ab5eeb553 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command.ts @@ -0,0 +1,30 @@ +import { Command, CommandRunner } from 'nest-commander'; + +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { cleanSuspendedWorkspaceCronPattern } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.cron.pattern'; +import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job'; + +@Command({ + name: 'cron:clean-suspended-workspaces', + description: 'Starts a cron job to clean suspended workspaces', +}) +export class CleanSuspendedWorkspacesCronCommand extends CommandRunner { + constructor( + @InjectMessageQueue(MessageQueue.cronQueue) + private readonly messageQueueService: MessageQueueService, + ) { + super(); + } + + async run(): Promise { + await this.messageQueueService.addCron( + CleanSuspendedWorkspacesJob.name, + undefined, + { + repeat: { pattern: cleanSuspendedWorkspaceCronPattern }, + }, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant.ts new file mode 100644 index 000000000..eadde01ae --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant.ts @@ -0,0 +1,2 @@ +export const USER_WORKSPACE_DELETION_WARNING_SENT_KEY = + 'USER_WORKSPACE_DELETION_WARNING_SENT'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.cron.pattern.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.cron.pattern.ts new file mode 100644 index 000000000..dc4d2036a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.cron.pattern.ts @@ -0,0 +1 @@ +export const cleanSuspendedWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm 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 new file mode 100644 index 000000000..f05f3a050 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts @@ -0,0 +1,210 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chunk from 'lodash.chunk'; +import { WorkspaceActivationStatus } from 'twenty-shared'; +import { Repository } from 'typeorm'; + +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; +import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24; + +@Processor(MessageQueue.cronQueue) +export class CleanSuspendedWorkspacesJob { + private readonly logger = new Logger(CleanSuspendedWorkspacesJob.name); + private readonly inactiveDaysBeforeDelete: number; + private readonly inactiveDaysBeforeWarn: number; + private readonly maxNumberOfWorkspacesDeletedPerExecution: number; + + constructor( + private readonly workspaceService: WorkspaceService, + private readonly environmentService: EnvironmentService, + private readonly userService: UserService, + private readonly userVarsService: UserVarsService, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) { + this.inactiveDaysBeforeDelete = this.environmentService.get( + 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', + ); + this.inactiveDaysBeforeWarn = this.environmentService.get( + 'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION', + ); + this.maxNumberOfWorkspacesDeletedPerExecution = this.environmentService.get( + 'MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION', + ); + } + + async computeWorkspaceBillingInactivity( + workspace: Workspace, + ): Promise { + try { + const lastSubscription = + await this.billingSubscriptionRepository.findOneOrFail({ + where: { workspaceId: workspace.id }, + order: { updatedAt: 'DESC' }, + }); + + const daysSinceBillingInactivity = Math.floor( + (new Date().getTime() - lastSubscription.updatedAt.getTime()) / + MILLISECONDS_IN_ONE_DAY, + ); + + return daysSinceBillingInactivity; + } catch { + this.logger.error( + `No billing subscription found for workspace ${workspace.id} ${workspace.displayName}`, + ); + + return null; + } + } + + async checkIfWorkspaceMembersWarned( + workspaceMembers: WorkspaceMemberWorkspaceEntity[], + workspaceId: string, + ) { + for (const workspaceMember of workspaceMembers) { + const workspaceMemberWarned = + (await this.userVarsService.get({ + userId: workspaceMember.userId, + workspaceId: workspaceId, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + })) === true; + + if (workspaceMemberWarned) { + return true; + } + } + + return false; + } + + async warnWorkspaceMembers(workspace: Workspace) { + const workspaceMembers = + await this.userService.loadWorkspaceMembers(workspace); + + const workspaceMembersWarned = await this.checkIfWorkspaceMembersWarned( + workspaceMembers, + workspace.id, + ); + + if (workspaceMembersWarned) { + this.logger.log( + `Workspace ${workspace.id} ${workspace.displayName} already warned`, + ); + + return; + } else { + const workspaceMembersChunks = chunk(workspaceMembers, 5); + + for (const workspaceMembersChunk of workspaceMembersChunks) { + await Promise.all( + workspaceMembersChunk.map(async (workspaceMember) => { + await this.userVarsService.set({ + userId: workspaceMember.userId, + workspaceId: workspace.id, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + value: true, + }); + }), + ); + } + + // TODO: issue #284 + // send email warning for deletion in (this.inactiveDaysBeforeDelete - this.inactiveDaysBeforeWarn) days (cci @twenty.com) + + this.logger.log( + `Warning Workspace ${workspace.id} ${workspace.displayName}`, + ); + + return; + } + } + + async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) { + const workspaceMembers = + await this.userService.loadWorkspaceMembers(workspace); + + const workspaceMembersChunks = chunk(workspaceMembers, 5); + + for (const workspaceMembersChunk of workspaceMembersChunks) { + await Promise.all( + workspaceMembersChunk.map(async (workspaceMember) => { + await this.userVarsService.delete({ + userId: workspaceMember.userId, + workspaceId: workspace.id, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + }); + }), + + // TODO: issue #285 + // send email informing about deletion (cci @twenty.com) + // remove clean-inactive-workspace.job.ts and .. files + // add new env var in infra + ); + } + + await this.workspaceService.deleteWorkspace(workspace.id); + this.logger.log( + `Cleaning Workspace ${workspace.id} ${workspace.displayName}`, + ); + } + + @Process(CleanSuspendedWorkspacesJob.name) + async handle(): Promise { + this.logger.log(`Job running...`); + + const suspendedWorkspaces = await this.workspaceRepository.find({ + where: { activationStatus: WorkspaceActivationStatus.SUSPENDED }, + }); + + const suspendedWorkspacesChunks = chunk(suspendedWorkspaces, 5); + + let deletedWorkspacesCount = 0; + + for (const suspendedWorkspacesChunk of suspendedWorkspacesChunks) { + await Promise.all( + suspendedWorkspacesChunk.map(async (workspace) => { + const workspaceInactivity = + await this.computeWorkspaceBillingInactivity(workspace); + + if ( + workspaceInactivity && + workspaceInactivity > this.inactiveDaysBeforeDelete && + deletedWorkspacesCount <= + this.maxNumberOfWorkspacesDeletedPerExecution + ) { + await this.informWorkspaceMembersAndDeleteWorkspace(workspace); + deletedWorkspacesCount++; + + return; + } + if ( + workspaceInactivity && + workspaceInactivity > this.inactiveDaysBeforeWarn && + workspaceInactivity <= this.inactiveDaysBeforeDelete + ) { + await this.warnWorkspaceMembers(workspace); + + return; + } + }), + ); + } + + this.logger.log(`Job done!`); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job.ts new file mode 100644 index 000000000..ce28665f9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job.ts @@ -0,0 +1,74 @@ +import { Logger, Scope } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chunk from 'lodash.chunk'; +import { Repository } from 'typeorm'; + +import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant'; + +export type CleanWorkspaceDeletionWarningUserVarsJobData = { + workspaceId: string; +}; + +@Processor({ + queueName: MessageQueue.workspaceQueue, + scope: Scope.REQUEST, +}) +export class CleanWorkspaceDeletionWarningUserVarsJob { + protected readonly logger = new Logger( + CleanWorkspaceDeletionWarningUserVarsJob.name, + ); + + constructor( + private readonly userService: UserService, + private readonly userVarsService: UserVarsService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) {} + + @Process(CleanWorkspaceDeletionWarningUserVarsJob.name) + async handle( + data: CleanWorkspaceDeletionWarningUserVarsJobData, + ): Promise { + this.logger.log(`Job running...`); + + const { workspaceId } = data; + + try { + const workspace = await this.workspaceRepository.findOneOrFail({ + where: { id: workspaceId }, + }); + + const workspaceMembers = + await this.userService.loadWorkspaceMembers(workspace); + + const workspaceMembersChunks = chunk(workspaceMembers, 5); + + for (const workspaceMembersChunk of workspaceMembersChunks) { + await Promise.all( + workspaceMembersChunk.map(async (workspaceMember) => { + await this.userVarsService.delete({ + userId: workspaceMember.userId, + workspaceId: workspace.id, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + }); + this.logger.log( + `Successfully cleaned user vars for ${workspaceMember.userId} user in ${workspace.id} workspace`, + ); + }), + ); + } + this.logger.log(`Job done!`); + } catch (error) { + this.logger.error( + `Failed to clean ${workspaceId} workspace users deletion warning user vars: ${error.message}`, + ); + } + } +} 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 681755be1..34da9983e 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 @@ -1,13 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { CleanInactiveWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-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 { StartCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command'; import { StopCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace- CleanInactiveWorkspacesCommand, StartCleanInactiveWorkspacesCronCommand, StopCleanInactiveWorkspacesCronCommand, + CleanSuspendedWorkspacesCronCommand, ], }) export class WorkspaceCleanerModule {}