2252 build a script to cleanup inactive workspaces (#3307)
* Add cron to message queue interfaces * Add command to launch cron job * Add command to stop cron job * Update clean inactive workspaces job * Add react-email * WIP * Fix import error * Rename services * Update logging * Update email template * Update email template * Add Base Email template * Move to proper place * Remove test files * Update logo * Add email theme * Revert "Remove test files" This reverts commit fe062dd05166b95125cf99f2165cc20efb6c275a. * Add email theme 2 * Revert "Revert "Remove test files"" This reverts commit 6c6471273ad765788f2eaf5a5614209edfb965ce. * Revert "Revert "Revert "Remove test files""" This reverts commit f851333c24e9cfe3f425c9cbbd1e079efce5c3dd. * Revert "Revert "Revert "Revert "Remove test files"""" This reverts commit 7838e19e88e269026e24803f26cd52b467b4ef36. * Fix theme * Reorganize files * Update clean inactive workspaces job * Use env variable to define inactive days * Remove FROM variable * Use feature flag * Fix cron command * Remove useless variable * Reorganize files * Refactor some code * Update email template * Update email object * Remove verbose log * Code review returns * Code review returns * Simplify handle * Code review returns * Review --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
@ -8,11 +8,11 @@ import { EmailSenderService } from 'src/integrations/email/email-sender.service'
|
||||
|
||||
@Injectable()
|
||||
export class EmailSenderJob implements MessageQueueJob<SendMailOptions> {
|
||||
private readonly logger = new Logger(EmailSenderJob.name);
|
||||
constructor(private readonly emailSenderService: EmailSenderService) {}
|
||||
|
||||
async handle(data: SendMailOptions): Promise<void> {
|
||||
process.stdout.write(`Sending email to ${data.to} ...`);
|
||||
await this.emailSenderService.send(data);
|
||||
console.log(' done!');
|
||||
this.logger.log(`Email to ${data.to} sent`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
|
||||
export const IsStrictlyLowerThan = (
|
||||
property: string,
|
||||
validationOptions?: ValidationOptions,
|
||||
) => {
|
||||
return (object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: 'isStrictlyLowerThan',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [property],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const [relatedPropertyName] = args.constraints;
|
||||
const relatedValue = (args.object as any)[relatedPropertyName];
|
||||
|
||||
return (
|
||||
typeof value === 'number' &&
|
||||
typeof relatedValue === 'number' &&
|
||||
value < relatedValue
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -172,6 +172,27 @@ export class EnvironmentService {
|
||||
);
|
||||
}
|
||||
|
||||
getEmailFromAddress(): string {
|
||||
return (
|
||||
this.configService.get<string>('EMAIL_FROM_ADDRESS') ??
|
||||
'noreply@yourdomain.com'
|
||||
);
|
||||
}
|
||||
|
||||
getEmailSystemAddress(): string {
|
||||
return (
|
||||
this.configService.get<string>('EMAIL_SYSTEM_ADDRESS') ??
|
||||
'system@yourdomain.com'
|
||||
);
|
||||
}
|
||||
|
||||
getEmailFromName(): string {
|
||||
return (
|
||||
this.configService.get<string>('EMAIL_FROM_NAME') ??
|
||||
'John from YourDomain'
|
||||
);
|
||||
}
|
||||
|
||||
getEmailDriver(): EmailDriver {
|
||||
return (
|
||||
this.configService.get<EmailDriver>('EMAIL_DRIVER') ?? EmailDriver.Logger
|
||||
@ -245,6 +266,18 @@ export class EnvironmentService {
|
||||
return this.configService.get<string | undefined>('OPENROUTER_API_KEY');
|
||||
}
|
||||
|
||||
getInactiveDaysBeforeEmail(): number | undefined {
|
||||
return this.configService.get<number | undefined>(
|
||||
'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION',
|
||||
);
|
||||
}
|
||||
|
||||
getInactiveDaysBeforeDelete(): number | undefined {
|
||||
return this.configService.get<number | undefined>(
|
||||
'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION',
|
||||
);
|
||||
}
|
||||
|
||||
isSignUpDisabled(): boolean {
|
||||
return this.configService.get<boolean>('IS_SIGN_UP_DISABLED') ?? false;
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import { CastToStringArray } from 'src/integrations/environment/decorators/cast-
|
||||
import { ExceptionHandlerDriver } from 'src/integrations/exception-handler/interfaces';
|
||||
import { StorageDriverType } from 'src/integrations/file-storage/interfaces';
|
||||
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
|
||||
import { IsStrictlyLowerThan } from 'src/integrations/environment/decorators/is-strictly-lower-than.decorator';
|
||||
|
||||
import { IsDuration } from './decorators/is-duration.decorator';
|
||||
import { AwsRegion } from './interfaces/aws-region.interface';
|
||||
@ -171,6 +172,21 @@ export class EnvironmentVariables {
|
||||
@IsString()
|
||||
SENTRY_DSN?: string;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
|
||||
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', {
|
||||
message:
|
||||
'"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: number;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION > 0)
|
||||
WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION: number;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { LogLevel } from '@nestjs/common';
|
||||
|
||||
export enum LoggerDriverType {
|
||||
Console = 'console',
|
||||
}
|
||||
|
||||
export interface ConsoleDriverFactoryOptions {
|
||||
type: LoggerDriverType.Console;
|
||||
logLevels?: LogLevel[];
|
||||
}
|
||||
|
||||
export type LoggerModuleOptions = ConsoleDriverFactoryOptions;
|
||||
|
||||
@ -13,11 +13,13 @@ export const loggerModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
): Promise<LoggerModuleOptions> => {
|
||||
const driverType = environmentService.getLoggerDriverType();
|
||||
const logLevels = environmentService.getLogLevels();
|
||||
|
||||
switch (driverType) {
|
||||
case LoggerDriverType.Console: {
|
||||
return {
|
||||
type: LoggerDriverType.Console,
|
||||
logLevels: logLevels,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
||||
@ -42,9 +42,16 @@ export class LoggerModule extends ConfigurableModuleClass {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config?.type === LoggerDriverType.Console
|
||||
? new ConsoleLogger()
|
||||
: undefined;
|
||||
const logLevels = config.logLevels ?? [];
|
||||
|
||||
const logger =
|
||||
config?.type === LoggerDriverType.Console
|
||||
? new ConsoleLogger()
|
||||
: undefined;
|
||||
|
||||
logger?.setLogLevels(logLevels);
|
||||
|
||||
return logger;
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export interface MessageQueueJob<T extends MessageQueueJobData> {
|
||||
export interface MessageQueueJob<T extends MessageQueueJobData | undefined> {
|
||||
handle(data: T): Promise<void> | void;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { GmailFullSyncJob } from 'src/workspace/messaging/jobs/gmail-full-sync.job';
|
||||
import { CallWebhookJobsJob } from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job';
|
||||
@ -8,10 +9,14 @@ 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 { 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';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -21,6 +26,10 @@ import { EmailSenderJob } from 'src/integrations/email/email-sender.job';
|
||||
HttpModule,
|
||||
TypeORMModule,
|
||||
FetchWorkspaceMessagesModule,
|
||||
UserModule,
|
||||
EnvironmentModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@ -40,9 +49,10 @@ import { EmailSenderJob } from 'src/integrations/email/email-sender.job';
|
||||
useClass: CallWebhookJob,
|
||||
},
|
||||
{
|
||||
provide: EmailSenderJob.name,
|
||||
useClass: EmailSenderJob,
|
||||
provide: CleanInactiveWorkspaceJob.name,
|
||||
useClass: CleanInactiveWorkspaceJob,
|
||||
},
|
||||
{ provide: EmailSenderJob.name, useClass: EmailSenderJob },
|
||||
],
|
||||
})
|
||||
export class JobsModule {
|
||||
|
||||
Reference in New Issue
Block a user