Etienne
2025-01-24 11:10:52 +01:00
committed by GitHub
parent 15814d465a
commit edd7212f0b
10 changed files with 393 additions and 17 deletions

View File

@ -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 { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; 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 { 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 { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/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, FeatureFlagModule,
StripeModule, StripeModule,
DomainManagerModule, DomainManagerModule,
MessageQueueModule,
TypeOrmModule.forFeature( TypeOrmModule.forFeature(
[ [
BillingSubscription, BillingSubscription,

View File

@ -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 { 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 { 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 { 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 { 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() @Injectable()
export class BillingWebhookSubscriptionService { export class BillingWebhookSubscriptionService {
protected readonly logger = new Logger( protected readonly logger = new Logger(
@ -21,6 +42,8 @@ export class BillingWebhookSubscriptionService {
); );
constructor( constructor(
private readonly stripeCustomerService: StripeCustomerService, private readonly stripeCustomerService: StripeCustomerService,
@InjectMessageQueue(MessageQueue.workspaceQueue)
private readonly messageQueueService: MessageQueueService,
@InjectRepository(BillingSubscription, 'core') @InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository<BillingSubscription>, private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
@InjectRepository(BillingSubscriptionItem, 'core') @InjectRepository(BillingSubscriptionItem, 'core')
@ -62,14 +85,28 @@ export class BillingWebhookSubscriptionService {
}, },
); );
const billingSubscription = const billingSubscriptions = await this.billingSubscriptionRepository.find({
await this.billingSubscriptionRepository.findOneOrFail({ where: { workspaceId },
where: { stripeSubscriptionId: data.object.id }, });
});
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( await this.billingSubscriptionItemRepository.upsert(
transformStripeSubscriptionEventToDatabaseSubscriptionItem( transformStripeSubscriptionEventToDatabaseSubscriptionItem(
billingSubscription.id, updatedBillingSubscription.id,
data, data,
), ),
{ {
@ -79,9 +116,10 @@ export class BillingWebhookSubscriptionService {
); );
if ( if (
data.object.status === SubscriptionStatus.Canceled || BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[
data.object.status === SubscriptionStatus.Unpaid || WorkspaceActivationStatus.SUSPENDED
data.object.status === SubscriptionStatus.Paused ].includes(data.object.status as SubscriptionStatus) &&
!hasActiveWorkspaceCompatibleSubscription
) { ) {
await this.workspaceRepository.update(workspaceId, { await this.workspaceRepository.update(workspaceId, {
activationStatus: WorkspaceActivationStatus.SUSPENDED, activationStatus: WorkspaceActivationStatus.SUSPENDED,
@ -89,14 +127,19 @@ export class BillingWebhookSubscriptionService {
} }
if ( if (
(data.object.status === SubscriptionStatus.Active || BILLING_SUBSCRIPTION_STATUS_BY_WORKSPACE_ACTIVATION_STATUS[
data.object.status === SubscriptionStatus.Trialing || WorkspaceActivationStatus.ACTIVE
data.object.status === SubscriptionStatus.PastDue) && ].includes(data.object.status as SubscriptionStatus) &&
workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED workspace.activationStatus == WorkspaceActivationStatus.SUSPENDED
) { ) {
await this.workspaceRepository.update(workspaceId, { await this.workspaceRepository.update(workspaceId, {
activationStatus: WorkspaceActivationStatus.ACTIVE, activationStatus: WorkspaceActivationStatus.ACTIVE,
}); });
await this.messageQueueService.add<CleanWorkspaceDeletionWarningUserVarsJobData>(
CleanWorkspaceDeletionWarningUserVarsJob.name,
{ workspaceId },
);
} }
await this.stripeCustomerService.updateCustomerMetadataWorkspaceId( await this.stripeCustomerService.updateCustomerMetadataWorkspaceId(

View File

@ -371,12 +371,17 @@ export class EnvironmentVariables {
'"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower that "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"', '"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower that "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"',
}) })
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0) @ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 30; WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 7;
@CastToPositiveNumber() @CastToPositiveNumber()
@IsNumber() @IsNumber()
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION > 0) @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) @IsEnum(CaptchaDriverType)
@IsOptional() @IsOptional()

View File

@ -7,11 +7,13 @@ import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-w
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.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 { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job'; import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
import { EmailModule } from 'src/engine/core-modules/email/email.module'; import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.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 { UserModule } from 'src/engine/core-modules/user/user.module';
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; 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 { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { 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 { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module';
import { CalendarModule } from 'src/modules/calendar/calendar.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'; 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({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace, BillingSubscription], 'core'),
DataSourceModule, DataSourceModule,
ObjectMetadataModule, ObjectMetadataModule,
TypeORMModule, TypeORMModule,
UserModule, UserModule,
UserVarsModule,
EmailModule, EmailModule,
DataSeedDemoWorkspaceModule, DataSeedDemoWorkspaceModule,
BillingModule, BillingModule,
@ -55,10 +60,12 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
], ],
providers: [ providers: [
CleanInactiveWorkspaceJob, CleanInactiveWorkspaceJob,
CleanSuspendedWorkspacesJob,
EmailSenderJob, EmailSenderJob,
DataSeedDemoWorkspaceJob, DataSeedDemoWorkspaceJob,
UpdateSubscriptionQuantityJob, UpdateSubscriptionQuantityJob,
HandleWorkspaceMemberDeletedJob, HandleWorkspaceMemberDeletedJob,
CleanWorkspaceDeletionWarningUserVarsJob,
], ],
}) })
export class JobsModule { export class JobsModule {

View File

@ -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<void> {
await this.messageQueueService.addCron<undefined>(
CleanSuspendedWorkspacesJob.name,
undefined,
{
repeat: { pattern: cleanSuspendedWorkspaceCronPattern },
},
);
}
}

View File

@ -0,0 +1,2 @@
export const USER_WORKSPACE_DELETION_WARNING_SENT_KEY =
'USER_WORKSPACE_DELETION_WARNING_SENT';

View File

@ -0,0 +1 @@
export const cleanSuspendedWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm

View File

@ -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<BillingSubscription>,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
) {
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<number | null> {
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<void> {
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!`);
}
}

View File

@ -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<Workspace>,
) {}
@Process(CleanWorkspaceDeletionWarningUserVarsJob.name)
async handle(
data: CleanWorkspaceDeletionWarningUserVarsJobData,
): Promise<void> {
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}`,
);
}
}
}

View File

@ -1,13 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; 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 { 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 { 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 { 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 { 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({ @Module({
imports: [ imports: [
@ -20,6 +21,7 @@ import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-
CleanInactiveWorkspacesCommand, CleanInactiveWorkspacesCommand,
StartCleanInactiveWorkspacesCronCommand, StartCleanInactiveWorkspacesCronCommand,
StopCleanInactiveWorkspacesCronCommand, StopCleanInactiveWorkspacesCronCommand,
CleanSuspendedWorkspacesCronCommand,
], ],
}) })
export class WorkspaceCleanerModule {} export class WorkspaceCleanerModule {}