3491 launch cleaning cron (#3872)

* Add command to delete incomplete workspaces

* Inject command dependencies

* Fix command

* Do not delete core.workspace

* Reorganize files

* Delete src/workspace/cron

* Fix

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2024-02-07 18:52:48 +01:00
committed by GitHub
parent 6e3a8e3461
commit 7001ca83d1
17 changed files with 132 additions and 37 deletions

View File

@ -2,12 +2,8 @@ import { Module } from '@nestjs/common';
import { DatabaseCommandModule } from 'src/database/commands/database-command.module';
import { FetchWorkspaceMessagesCommandsModule } from 'src/workspace/messaging/commands/fetch-workspace-messages-commands.module';
import { StartCleanInactiveWorkspacesCronCommand } from 'src/workspace/cron/clean-inactive-workspaces/commands/start-clean-inactive-workspaces.cron.command';
import { StopCleanInactiveWorkspacesCronCommand } from 'src/workspace/cron/clean-inactive-workspaces/commands/stop-clean-inactive-workspaces.cron.command';
import { CleanInactiveWorkspacesCommand } from 'src/workspace/cron/clean-inactive-workspaces/commands/clean-inactive-workspaces.command';
import { WorkspaceHealthCommandModule } from 'src/workspace/workspace-health/commands/workspace-health-command.module';
import { StartFetchAllWorkspacesMessagesCronCommand } from 'src/workspace/cron/fetch-all-workspaces-messages/commands/start-fetch-all-workspaces-messages.cron.command';
import { StopFetchAllWorkspacesMessagesCronCommand } from 'src/workspace/cron/fetch-all-workspaces-messages/commands/stop-fetch-all-workspaces-messages.cron.command';
import { WorkspaceCleanerModule } from 'src/workspace/workspace-cleaner/workspace-cleaner.module';
import { AppModule } from './app.module';
@ -20,13 +16,9 @@ import { WorkspaceMigrationRunnerCommandsModule } from './workspace/workspace-mi
WorkspaceSyncMetadataCommandsModule,
DatabaseCommandModule,
FetchWorkspaceMessagesCommandsModule,
StartCleanInactiveWorkspacesCronCommand,
StopCleanInactiveWorkspacesCronCommand,
CleanInactiveWorkspacesCommand,
WorkspaceCleanerModule,
WorkspaceHealthCommandModule,
WorkspaceMigrationRunnerCommandsModule,
StartFetchAllWorkspacesMessagesCronCommand,
StopFetchAllWorkspacesMessagesCronCommand,
],
})
export class CommandModule {}

View File

@ -17,13 +17,15 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
super(workspaceRepository);
}
async deleteWorkspace(id: string) {
async deleteWorkspace(id: string, shouldDeleteCoreWorkspace = true) {
const workspace = await this.workspaceRepository.findOneBy({ id });
assert(workspace, 'Workspace not found');
await this.workspaceManagerService.delete(id);
await this.workspaceRepository.delete(id);
if (shouldDeleteCoreWorkspace) {
await this.workspaceRepository.delete(id);
}
return workspace;
}

View File

@ -9,7 +9,7 @@ import { CallWebhookJob } from 'src/workspace/workspace-query-runner/jobs/call-w
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { CleanInactiveWorkspaceJob } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.job';
import { CleanInactiveWorkspaceJob } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.job';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FetchWorkspaceMessagesModule } from 'src/workspace/messaging/services/fetch-workspace-messages.module';
import { GmailPartialSyncJob } from 'src/workspace/messaging/jobs/gmail-partial-sync.job';
@ -17,7 +17,7 @@ import { EmailSenderJob } from 'src/integrations/email/email-sender.job';
import { UserModule } from 'src/core/user/user.module';
import { EnvironmentModule } from 'src/integrations/environment/environment.module';
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/cron/fetch-all-workspaces-messages/fetch-all-workspaces-messages.job';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.job';
import { ConnectedAccountModule } from 'src/workspace/messaging/connected-account/connected-account.module';
@Module({

View File

@ -0,0 +1,3 @@
export const getDryRunLogHeader = (isDryRun: boolean | undefined): string => {
return isDryRun ? 'Dry-run mode: ' : '';
};

View File

@ -7,6 +7,8 @@ import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
import { GmailFullSyncCommand } from 'src/workspace/messaging/commands/gmail-full-sync.command';
import { GmailPartialSyncCommand } from 'src/workspace/messaging/commands/gmail-partial-sync.command';
import { ConnectedAccountModule } from 'src/workspace/messaging/connected-account/connected-account.module';
import { StartFetchAllWorkspacesMessagesCronCommand } from 'src/workspace/messaging/commands/start-fetch-all-workspaces-messages.cron.command';
import { StopFetchAllWorkspacesMessagesCronCommand } from 'src/workspace/messaging/commands/stop-fetch-all-workspaces-messages.cron.command';
@Module({
imports: [
@ -15,6 +17,11 @@ import { ConnectedAccountModule } from 'src/workspace/messaging/connected-accoun
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
ConnectedAccountModule,
],
providers: [GmailFullSyncCommand, GmailPartialSyncCommand],
providers: [
GmailFullSyncCommand,
GmailPartialSyncCommand,
StartFetchAllWorkspacesMessagesCronCommand,
StopFetchAllWorkspacesMessagesCronCommand,
],
})
export class FetchWorkspaceMessagesCommandsModule {}

View File

@ -4,8 +4,8 @@ import { Command, CommandRunner } from 'nest-commander';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { fetchAllWorkspacesMessagesCronPattern } from 'src/workspace/cron/fetch-all-workspaces-messages/fetch-all-workspaces-messages.cron.pattern';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/cron/fetch-all-workspaces-messages/fetch-all-workspaces-messages.job';
import { fetchAllWorkspacesMessagesCronPattern } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.cron.pattern';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.job';
@Command({
name: 'fetch-all-workspaces-messages:cron:start',

View File

@ -4,8 +4,8 @@ import { Command, CommandRunner } from 'nest-commander';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { fetchAllWorkspacesMessagesCronPattern } from 'src/workspace/cron/fetch-all-workspaces-messages/fetch-all-workspaces-messages.cron.pattern';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/cron/fetch-all-workspaces-messages/fetch-all-workspaces-messages.job';
import { fetchAllWorkspacesMessagesCronPattern } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.cron.pattern';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/crons/fetch-all-workspaces-messages.job';
@Command({
name: 'fetch-all-workspaces-messages:cron:stop',

View File

@ -4,7 +4,7 @@ import { Command, CommandRunner, Option } from 'nest-commander';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { CleanInactiveWorkspaceJob } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.job';
import { CleanInactiveWorkspaceJob } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.job';
export type CleanInactiveWorkspacesCommandOptions = {
dryRun: boolean;

View File

@ -0,0 +1,75 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Logger } from '@nestjs/common';
import { Command, CommandRunner, Option } from 'nest-commander';
import { FindOptionsWhere, Repository } from 'typeorm';
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
type DeleteIncompleteWorkspacesCommandOptions = {
dryRun?: boolean;
workspaceId?: string;
};
@Command({
name: 'workspace:delete-incomplete',
description: 'Delete incomplete workspaces',
})
export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
private readonly logger = new Logger(DeleteIncompleteWorkspacesCommand.name);
constructor(
private readonly workspaceService: WorkspaceService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
) {
super();
}
@Option({
flags: '-d, --dry-run [dry run]',
description: 'Dry run: Log delete actions without executing them.',
required: false,
})
dryRun(value: string): boolean {
return Boolean(value);
}
@Option({
flags: '-w, --workspace-id [workspace_id]',
description: 'workspace id',
required: false,
})
parseWorkspaceId(value: string): string {
return value;
}
async run(
_passedParam: string[],
options: DeleteIncompleteWorkspacesCommandOptions,
): Promise<void> {
const where: FindOptionsWhere<Workspace> = {
subscriptionStatus: 'incomplete',
};
if (options.workspaceId) {
where.id = options.workspaceId;
}
const incompleteWorkspaces = await this.workspaceRepository.findBy(where);
for (const incompleteWorkspace of incompleteWorkspaces) {
this.logger.log(
`${getDryRunLogHeader(options.dryRun)}Deleting workspace ${
incompleteWorkspace.id
} name: '${incompleteWorkspace.displayName}'`,
);
if (!options.dryRun) {
await this.workspaceService.deleteWorkspace(
incompleteWorkspace.id,
false,
);
}
}
}
}

View File

@ -4,8 +4,8 @@ import { Command, CommandRunner } from 'nest-commander';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { cleanInactiveWorkspaceCronPattern } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.cron.pattern';
import { CleanInactiveWorkspaceJob } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.job';
import { cleanInactiveWorkspaceCronPattern } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern';
import { CleanInactiveWorkspaceJob } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.job';
@Command({
name: 'clean-inactive-workspaces:cron:start',

View File

@ -4,8 +4,8 @@ import { Command, CommandRunner } from 'nest-commander';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { cleanInactiveWorkspaceCronPattern } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.cron.pattern';
import { CleanInactiveWorkspaceJob } from 'src/workspace/cron/clean-inactive-workspaces/clean-inactive-workspace.job';
import { cleanInactiveWorkspaceCronPattern } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.cron.pattern';
import { CleanInactiveWorkspaceJob } from 'src/workspace/workspace-cleaner/crons/clean-inactive-workspace.job';
@Command({
name: 'clean-inactive-workspaces:cron:stop',

View File

@ -23,7 +23,8 @@ import {
} from 'src/core/feature-flag/feature-flag.entity';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
import { CleanInactiveWorkspacesCommandOptions } from 'src/workspace/cron/clean-inactive-workspaces/commands/clean-inactive-workspaces.command';
import { CleanInactiveWorkspacesCommandOptions } from 'src/workspace/workspace-cleaner/commands/clean-inactive-workspaces.command';
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24;
@ -112,7 +113,7 @@ export class CleanInactiveWorkspaceJob
)?.[0].displayName;
this.logger.log(
`${this.getDryRunLogHeader(isDryRun)}Sending workspace ${
`${getDryRunLogHeader(isDryRun)}Sending workspace ${
dataSource.workspaceId
} inactive since ${daysSinceInactive} days emails to users ['${workspaceMembers
.map((workspaceUser) => workspaceUser.email)
@ -162,10 +163,6 @@ export class CleanInactiveWorkspaceJob
);
}
getDryRunLogHeader(isDryRun: boolean): string {
return isDryRun ? 'Dry-run mode: ' : '';
}
chunkArray(array: any[], chunkSize = 6): any[][] {
const chunkedArray: any[][] = [];
let index = 0;
@ -183,7 +180,7 @@ export class CleanInactiveWorkspaceJob
isDryRun: boolean,
): Promise<void> {
this.logger.log(
`${this.getDryRunLogHeader(
`${getDryRunLogHeader(
isDryRun,
)}Sending email to delete workspaces "${workspacesToDelete
.map((workspaceToDelete) => workspaceToDelete.workspaceId)
@ -216,7 +213,7 @@ export class CleanInactiveWorkspaceJob
const workspacesToDelete: WorkspaceToDeleteData[] = [];
this.logger.log(`${this.getDryRunLogHeader(isDryRun)}Job running...`);
this.logger.log(`${getDryRunLogHeader(isDryRun)}Job running...`);
if (!this.inactiveDaysBeforeDelete && !this.inactiveDaysBeforeEmail) {
this.logger.log(
`'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION' and 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION' environment variables not set, please check this doc for more info: https://docs.twenty.com/start/self-hosting/environment-variables`,
@ -231,7 +228,7 @@ export class CleanInactiveWorkspaceJob
const dataSourcesChunks = this.chunkArray(dataSources);
this.logger.log(
`${this.getDryRunLogHeader(isDryRun)}On ${
`${getDryRunLogHeader(isDryRun)}On ${
dataSources.length
} workspaces divided in ${dataSourcesChunks.length} chunks...`,
);
@ -246,7 +243,7 @@ export class CleanInactiveWorkspaceJob
for (const dataSource of dataSourcesChunk) {
if (!(await this.isWorkspaceCleanable(dataSource))) {
this.logger.log(
`${this.getDryRunLogHeader(isDryRun)}Workspace ${
`${getDryRunLogHeader(isDryRun)}Workspace ${
dataSource.workspaceId
} not cleanable`,
);
@ -254,7 +251,7 @@ export class CleanInactiveWorkspaceJob
}
this.logger.log(
`${this.getDryRunLogHeader(isDryRun)}Cleaning Workspace ${
`${getDryRunLogHeader(isDryRun)}Cleaning Workspace ${
dataSource.workspaceId
}`,
);
@ -285,6 +282,6 @@ export class CleanInactiveWorkspaceJob
await this.sendDeleteWorkspaceEmail(workspacesToDelete, isDryRun);
this.logger.log(`${this.getDryRunLogHeader(isDryRun)}job done!`);
this.logger.log(`${getDryRunLogHeader(isDryRun)}job done!`);
}
}

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
import { DeleteIncompleteWorkspacesCommand } from 'src/workspace/workspace-cleaner/commands/delete-incomplete-workspaces.command';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { CleanInactiveWorkspacesCommand } from 'src/workspace/workspace-cleaner/commands/clean-inactive-workspaces.command';
import { StartCleanInactiveWorkspacesCronCommand } from 'src/workspace/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command';
import { StopCleanInactiveWorkspacesCronCommand } from 'src/workspace/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command';
@Module({
imports: [TypeOrmModule.forFeature([Workspace], 'core'), WorkspaceModule],
providers: [
DeleteIncompleteWorkspacesCommand,
CleanInactiveWorkspacesCommand,
StartCleanInactiveWorkspacesCronCommand,
StopCleanInactiveWorkspacesCronCommand,
],
})
export class WorkspaceCleanerModule {}

View File

@ -38,7 +38,6 @@ const generateOpportunities = (
amountCurrencyCode: 'USD',
closeDate: new Date(),
stage: getRandomStage(),
position: null,
probability: getRandomProbability(),
pipelineStepId: getRandomPipelineStepId(pipelineStepIds),
pointOfContactId: company.personId,