Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,66 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
import { CastToLogLevelArray } from 'src/integrations/environment/decorators/cast-to-log-level-array.decorator';
|
||||
|
||||
class TestClass {
|
||||
@CastToLogLevelArray()
|
||||
logLevels?: any;
|
||||
}
|
||||
|
||||
describe('CastToLogLevelArray Decorator', () => {
|
||||
it('should cast "log" to ["log"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'log' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['log']);
|
||||
});
|
||||
|
||||
it('should cast "error" to ["error"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'error' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['error']);
|
||||
});
|
||||
|
||||
it('should cast "warn" to ["warn"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'warn' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['warn']);
|
||||
});
|
||||
|
||||
it('should cast "debug" to ["debug"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'debug' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['debug']);
|
||||
});
|
||||
|
||||
it('should cast "verbose" to ["verbose"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'verbose' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['verbose']);
|
||||
});
|
||||
|
||||
it('should cast "verbose,error,warn" to ["verbose", "error", "warn"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
logLevels: 'verbose,error,warn',
|
||||
});
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual([
|
||||
'verbose',
|
||||
'error',
|
||||
'warn',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should cast "toto" to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'toto' });
|
||||
|
||||
expect(transformedClass.logLevels).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should cast "verbose,error,toto" to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
logLevels: 'verbose,error,toto',
|
||||
});
|
||||
|
||||
expect(transformedClass.logLevels).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
import { CastToPositiveNumber } from 'src/integrations/environment/decorators/cast-to-positive-number.decorator';
|
||||
|
||||
class TestClass {
|
||||
@CastToPositiveNumber()
|
||||
numberProperty?: any;
|
||||
}
|
||||
|
||||
describe('CastToPositiveNumber Decorator', () => {
|
||||
it('should cast number to number', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: 123 });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(123);
|
||||
});
|
||||
|
||||
it('should cast string to number', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: '123' });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(123);
|
||||
});
|
||||
|
||||
it('should cast null to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: null });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast negative number to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: -12 });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast undefined to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: undefined,
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast NaN string to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: 'toto',
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast a negative string to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: '-123',
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToBoolean = () =>
|
||||
Transform(({ value }: { value: string }) => toBoolean(value));
|
||||
|
||||
const toBoolean = (value: any) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToLogLevelArray = () =>
|
||||
Transform(({ value }: { value: string }) => toLogLevelArray(value));
|
||||
|
||||
const toLogLevelArray = (value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
const rawLogLevels = value.split(',').map((level) => level.trim());
|
||||
const isInvalid = rawLogLevels.some(
|
||||
(level) => !['log', 'error', 'warn', 'debug', 'verbose'].includes(level),
|
||||
);
|
||||
|
||||
if (!isInvalid) {
|
||||
return rawLogLevels;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToPositiveNumber = () =>
|
||||
Transform(({ value }: { value: string }) => toNumber(value));
|
||||
|
||||
const toNumber = (value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return value >= 0 ? value : undefined;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return isNaN(+value) ? undefined : toNumber(+value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToStringArray = () =>
|
||||
Transform(({ value }: { value: string }) => toStringArray(value));
|
||||
|
||||
const toStringArray = (value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
return value.split(',').map((item) => item.trim());
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsAWSRegionConstraint implements ValidatorConstraintInterface {
|
||||
validate(region: string) {
|
||||
const regex = /^[a-z]{2}-[a-z]+-\d{1}$/;
|
||||
|
||||
return regex.test(region); // Returns true if region matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export const IsAWSRegion =
|
||||
(validationOptions?: ValidationOptions) =>
|
||||
(object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsAWSRegionConstraint,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsDurationConstraint implements ValidatorConstraintInterface {
|
||||
validate(duration: string) {
|
||||
const regex =
|
||||
/^-?[0-9]+(.[0-9]+)?(m(illiseconds?)?|s(econds?)?|h((ou)?rs?)?|d(ays?)?|w(eeks?)?|M(onths?)?|y(ears?)?)?$/;
|
||||
|
||||
return regex.test(duration); // Returns true if duration matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export const IsDuration =
|
||||
(validationOptions?: ValidationOptions) =>
|
||||
(object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsDurationConstraint,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
|
||||
new ConfigurableModuleBuilder({
|
||||
moduleName: 'Environment',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
@ -0,0 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { EnvironmentService } from './environment.service';
|
||||
import { ConfigurableModuleClass } from './environment.module-definition';
|
||||
import { validate } from './environment.validation';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
expandVariables: true,
|
||||
validate,
|
||||
}),
|
||||
],
|
||||
providers: [EnvironmentService],
|
||||
exports: [EnvironmentService],
|
||||
})
|
||||
export class EnvironmentModule extends ConfigurableModuleClass {}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { EnvironmentService } from './environment.service';
|
||||
|
||||
describe('EnvironmentService', () => {
|
||||
let service: EnvironmentService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EnvironmentService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EnvironmentService>(EnvironmentService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,211 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Injectable, LogLevel } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
|
||||
import { ExceptionHandlerDriver } from 'src/integrations/exception-handler/interfaces';
|
||||
import { StorageDriverType } from 'src/integrations/file-storage/interfaces';
|
||||
import { MessageQueueDriverType } from 'src/integrations/message-queue/interfaces';
|
||||
|
||||
import { AwsRegion } from './interfaces/aws-region.interface';
|
||||
import { SupportDriver } from './interfaces/support.interface';
|
||||
|
||||
@Injectable()
|
||||
export class EnvironmentService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
isDebugMode(): boolean {
|
||||
return this.configService.get<boolean>('DEBUG_MODE') ?? false;
|
||||
}
|
||||
|
||||
isSignInPrefilled(): boolean {
|
||||
return this.configService.get<boolean>('SIGN_IN_PREFILLED') ?? false;
|
||||
}
|
||||
|
||||
isTelemetryEnabled(): boolean {
|
||||
return this.configService.get<boolean>('TELEMETRY_ENABLED') ?? true;
|
||||
}
|
||||
|
||||
isTelemetryAnonymizationEnabled(): boolean {
|
||||
return (
|
||||
this.configService.get<boolean>('TELEMETRY_ANONYMIZATION_ENABLED') ?? true
|
||||
);
|
||||
}
|
||||
|
||||
getPort(): number {
|
||||
return this.configService.get<number>('PORT') ?? 3000;
|
||||
}
|
||||
|
||||
getPGDatabaseUrl(): string {
|
||||
return this.configService.get<string>('PG_DATABASE_URL')!;
|
||||
}
|
||||
|
||||
getRedisHost(): string {
|
||||
return this.configService.get<string>('REDIS_HOST') ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
getRedisPort(): number {
|
||||
return +(this.configService.get<string>('REDIS_PORT') ?? 6379);
|
||||
}
|
||||
|
||||
getFrontBaseUrl(): string {
|
||||
return this.configService.get<string>('FRONT_BASE_URL')!;
|
||||
}
|
||||
|
||||
getServerUrl(): string {
|
||||
return this.configService.get<string>('SERVER_URL')!;
|
||||
}
|
||||
|
||||
getAccessTokenSecret(): string {
|
||||
return this.configService.get<string>('ACCESS_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getAccessTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN') ?? '30m';
|
||||
}
|
||||
|
||||
getRefreshTokenSecret(): string {
|
||||
return this.configService.get<string>('REFRESH_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getRefreshTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('REFRESH_TOKEN_EXPIRES_IN') ?? '90d';
|
||||
}
|
||||
|
||||
getRefreshTokenCoolDown(): string {
|
||||
return this.configService.get<string>('REFRESH_TOKEN_COOL_DOWN') ?? '1m';
|
||||
}
|
||||
|
||||
getLoginTokenSecret(): string {
|
||||
return this.configService.get<string>('LOGIN_TOKEN_SECRET')!;
|
||||
}
|
||||
|
||||
getLoginTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN') ?? '15m';
|
||||
}
|
||||
|
||||
getTransientTokenExpiresIn(): string {
|
||||
return (
|
||||
this.configService.get<string>('SHORT_TERM_TOKEN_EXPIRES_IN') ?? '5m'
|
||||
);
|
||||
}
|
||||
|
||||
getApiTokenExpiresIn(): string {
|
||||
return this.configService.get<string>('API_TOKEN_EXPIRES_IN') ?? '1000y';
|
||||
}
|
||||
|
||||
getFrontAuthCallbackUrl(): string {
|
||||
return (
|
||||
this.configService.get<string>('FRONT_AUTH_CALLBACK_URL') ??
|
||||
this.getFrontBaseUrl() + '/verify'
|
||||
);
|
||||
}
|
||||
|
||||
isMessagingProviderGmailEnabled(): boolean {
|
||||
return (
|
||||
this.configService.get<boolean>('MESSAGING_PROVIDER_GMAIL_ENABLED') ??
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
getMessagingProviderGmailCallbackUrl(): string | undefined {
|
||||
return this.configService.get<string>(
|
||||
'MESSAGING_PROVIDER_GMAIL_CALLBACK_URL',
|
||||
);
|
||||
}
|
||||
|
||||
isAuthGoogleEnabled(): boolean {
|
||||
return this.configService.get<boolean>('AUTH_GOOGLE_ENABLED') ?? false;
|
||||
}
|
||||
|
||||
getAuthGoogleClientId(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CLIENT_ID');
|
||||
}
|
||||
|
||||
getAuthGoogleClientSecret(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CLIENT_SECRET');
|
||||
}
|
||||
|
||||
getAuthGoogleCallbackUrl(): string | undefined {
|
||||
return this.configService.get<string>('AUTH_GOOGLE_CALLBACK_URL');
|
||||
}
|
||||
|
||||
getStorageDriverType(): StorageDriverType {
|
||||
return (
|
||||
this.configService.get<StorageDriverType>('STORAGE_TYPE') ??
|
||||
StorageDriverType.Local
|
||||
);
|
||||
}
|
||||
|
||||
getMessageQueueDriverType(): MessageQueueDriverType {
|
||||
return (
|
||||
this.configService.get<MessageQueueDriverType>('MESSAGE_QUEUE_TYPE') ??
|
||||
MessageQueueDriverType.PgBoss
|
||||
);
|
||||
}
|
||||
|
||||
getStorageS3Region(): AwsRegion | undefined {
|
||||
return this.configService.get<AwsRegion>('STORAGE_S3_REGION');
|
||||
}
|
||||
|
||||
getStorageS3Name(): string | undefined {
|
||||
return this.configService.get<string>('STORAGE_S3_NAME');
|
||||
}
|
||||
|
||||
getStorageS3Endpoint(): string | undefined {
|
||||
return this.configService.get<string>('STORAGE_S3_ENDPOINT');
|
||||
}
|
||||
|
||||
getStorageLocalPath(): string {
|
||||
return (
|
||||
this.configService.get<string>('STORAGE_LOCAL_PATH') ?? '.local-storage'
|
||||
);
|
||||
}
|
||||
|
||||
getSupportDriver(): string {
|
||||
return (
|
||||
this.configService.get<string>('SUPPORT_DRIVER') ?? SupportDriver.None
|
||||
);
|
||||
}
|
||||
|
||||
getSupportFrontChatId(): string | undefined {
|
||||
return this.configService.get<string>('SUPPORT_FRONT_CHAT_ID');
|
||||
}
|
||||
|
||||
getSupportFrontHMACKey(): string | undefined {
|
||||
return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY');
|
||||
}
|
||||
|
||||
getLoggerDriverType(): LoggerDriverType {
|
||||
return (
|
||||
this.configService.get<LoggerDriverType>('LOGGER_DRIVER') ??
|
||||
LoggerDriverType.Console
|
||||
);
|
||||
}
|
||||
|
||||
getExceptionHandlerDriverType(): ExceptionHandlerDriver {
|
||||
return (
|
||||
this.configService.get<ExceptionHandlerDriver>(
|
||||
'EXCEPTION_HANDLER_DRIVER',
|
||||
) ?? ExceptionHandlerDriver.Console
|
||||
);
|
||||
}
|
||||
|
||||
getLogLevels(): LogLevel[] {
|
||||
return (
|
||||
this.configService.get<LogLevel[]>('LOG_LEVELS') ?? [
|
||||
'log',
|
||||
'error',
|
||||
'warn',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
getSentryDSN(): string | undefined {
|
||||
return this.configService.get<string>('SENTRY_DSN');
|
||||
}
|
||||
|
||||
getDemoWorkspaceIds(): string[] {
|
||||
return this.configService.get<string[]>('DEMO_WORKSPACE_IDS') ?? [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
import { LogLevel } from '@nestjs/common';
|
||||
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateIf,
|
||||
validateSync,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { CastToStringArray } from 'src/integrations/environment/decorators/cast-to-string-array.decorator';
|
||||
import { ExceptionHandlerDriver } from 'src/integrations/exception-handler/interfaces';
|
||||
import { StorageDriverType } from 'src/integrations/file-storage/interfaces';
|
||||
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
|
||||
|
||||
import { IsDuration } from './decorators/is-duration.decorator';
|
||||
import { AwsRegion } from './interfaces/aws-region.interface';
|
||||
import { IsAWSRegion } from './decorators/is-aws-region.decorator';
|
||||
import { CastToBoolean } from './decorators/cast-to-boolean.decorator';
|
||||
import { SupportDriver } from './interfaces/support.interface';
|
||||
import { CastToPositiveNumber } from './decorators/cast-to-positive-number.decorator';
|
||||
import { CastToLogLevelArray } from './decorators/cast-to-log-level-array.decorator';
|
||||
|
||||
export class EnvironmentVariables {
|
||||
// Misc
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
DEBUG_MODE?: boolean;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
SIGN_IN_PREFILLED?: boolean;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
TELEMETRY_ENABLED?: boolean;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
TELEMETRY_ANONYMIZATION_ENABLED?: boolean;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PORT: number;
|
||||
|
||||
// Database
|
||||
@IsUrl({ protocols: ['postgres'], require_tld: false })
|
||||
PG_DATABASE_URL: string;
|
||||
|
||||
// Frontend URL
|
||||
@IsUrl({ require_tld: false })
|
||||
FRONT_BASE_URL: string;
|
||||
|
||||
// Server URL
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
SERVER_URL: string;
|
||||
|
||||
// Json Web Token
|
||||
@IsString()
|
||||
ACCESS_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
ACCESS_TOKEN_EXPIRES_IN: string;
|
||||
|
||||
@IsString()
|
||||
REFRESH_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
REFRESH_TOKEN_EXPIRES_IN: string;
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
REFRESH_TOKEN_COOL_DOWN: string;
|
||||
|
||||
@IsString()
|
||||
LOGIN_TOKEN_SECRET: string;
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
LOGIN_TOKEN_EXPIRES_IN: string;
|
||||
|
||||
// Auth
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
FRONT_AUTH_CALLBACK_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_GOOGLE_ENABLED?: boolean;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
AUTH_GOOGLE_CLIENT_ID?: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
AUTH_GOOGLE_CLIENT_SECRET?: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
AUTH_GOOGLE_CALLBACK_URL?: string;
|
||||
|
||||
// Storage
|
||||
@IsEnum(StorageDriverType)
|
||||
@IsOptional()
|
||||
STORAGE_TYPE?: StorageDriverType;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsAWSRegion()
|
||||
STORAGE_S3_REGION?: AwsRegion;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsString()
|
||||
STORAGE_S3_NAME?: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
|
||||
STORAGE_LOCAL_PATH?: string;
|
||||
|
||||
// Support
|
||||
@IsEnum(SupportDriver)
|
||||
@IsOptional()
|
||||
SUPPORT_DRIVER?: SupportDriver;
|
||||
|
||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||
@IsString()
|
||||
SUPPORT_FRONT_CHAT_ID?: string;
|
||||
|
||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||
@IsString()
|
||||
SUPPORT_FRONT_HMAC_KEY?: string;
|
||||
|
||||
@IsEnum(LoggerDriverType)
|
||||
@IsOptional()
|
||||
LOGGER_DRIVER?: LoggerDriverType;
|
||||
|
||||
@IsEnum(ExceptionHandlerDriver)
|
||||
@IsOptional()
|
||||
EXCEPTION_HANDLER_DRIVER?: ExceptionHandlerDriver;
|
||||
|
||||
@CastToLogLevelArray()
|
||||
@IsOptional()
|
||||
LOG_LEVELS?: LogLevel[];
|
||||
|
||||
@CastToStringArray()
|
||||
@IsOptional()
|
||||
DEMO_WORKSPACE_IDS?: string[];
|
||||
|
||||
@ValidateIf(
|
||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||
)
|
||||
@IsString()
|
||||
SENTRY_DSN?: string;
|
||||
}
|
||||
|
||||
export const validate = (config: Record<string, unknown>) => {
|
||||
const validatedConfig = plainToClass(EnvironmentVariables, config);
|
||||
|
||||
const errors = validateSync(validatedConfig);
|
||||
|
||||
assert(!errors.length, errors.toString());
|
||||
|
||||
return validatedConfig;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type AwsRegion = `${string}-${string}-${number}`;
|
||||
@ -0,0 +1,4 @@
|
||||
export enum SupportDriver {
|
||||
None = 'none',
|
||||
Front = 'front',
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerConsoleDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
captureException(exception: unknown) {
|
||||
console.group('Exception Captured');
|
||||
console.error(exception);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
captureMessage(message: string): void {
|
||||
console.group('Message Captured');
|
||||
console.info(message);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { ProfilingIntegration } from '@sentry/profiling-node';
|
||||
|
||||
import {
|
||||
ExceptionHandlerDriverInterface,
|
||||
ExceptionHandlerSentryDriverFactoryOptions,
|
||||
} from 'src/integrations/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerSentryDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
constructor(options: ExceptionHandlerSentryDriverFactoryOptions['options']) {
|
||||
Sentry.init({
|
||||
dsn: options.dns,
|
||||
integrations: [
|
||||
// enable HTTP calls tracing
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
// enable Express.js middleware tracing
|
||||
new Sentry.Integrations.Express({ app: options.serverInstance }),
|
||||
new Sentry.Integrations.GraphQL(),
|
||||
new Sentry.Integrations.Postgres({
|
||||
usePgNative: true,
|
||||
}),
|
||||
new ProfilingIntegration(),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
profilesSampleRate: 1.0,
|
||||
environment: options.debug ? 'development' : 'production',
|
||||
debug: options.debug,
|
||||
});
|
||||
}
|
||||
|
||||
captureException(exception: Error) {
|
||||
Sentry.captureException(exception);
|
||||
}
|
||||
|
||||
captureMessage(message: string) {
|
||||
Sentry.captureMessage(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const EXCEPTION_HANDLER_DRIVER = Symbol('EXCEPTION_HANDLER_DRIVER');
|
||||
@ -0,0 +1,25 @@
|
||||
import {
|
||||
ConfigurableModuleBuilder,
|
||||
FactoryProvider,
|
||||
ModuleMetadata,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerModuleOptions } from './interfaces';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<ExceptionHandlerModuleOptions>({
|
||||
moduleName: 'ExceptionHandlerModule',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
|
||||
export type ExceptionHandlerModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => ExceptionHandlerModuleOptions | Promise<ExceptionHandlerModuleOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1,39 @@
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import { OPTIONS_TYPE } from 'src/integrations/exception-handler/exception-handler.module-definition';
|
||||
import { ExceptionHandlerDriver } from 'src/integrations/exception-handler/interfaces';
|
||||
|
||||
/**
|
||||
* ExceptionHandler Module factory
|
||||
* @param environment
|
||||
* @returns ExceptionHandlerModuleOptions
|
||||
*/
|
||||
export const exceptionHandlerModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
adapterHost: HttpAdapterHost,
|
||||
): Promise<typeof OPTIONS_TYPE> => {
|
||||
const driverType = environmentService.getExceptionHandlerDriverType();
|
||||
|
||||
switch (driverType) {
|
||||
case ExceptionHandlerDriver.Console: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Console,
|
||||
};
|
||||
}
|
||||
case ExceptionHandlerDriver.Sentry: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Sentry,
|
||||
options: {
|
||||
dns: environmentService.getSentryDSN() ?? '',
|
||||
serverInstance: adapterHost.httpAdapter.getInstance(),
|
||||
debug: environmentService.isDebugMode(),
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid exception capturer driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerSentryDriver } from 'src/integrations/exception-handler/drivers/sentry.driver';
|
||||
import { ExceptionHandlerConsoleDriver } from 'src/integrations/exception-handler/drivers/console.driver';
|
||||
|
||||
import { ExceptionHandlerService } from './exception-handler.service';
|
||||
import { ExceptionHandlerDriver } from './interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants';
|
||||
import {
|
||||
ConfigurableModuleClass,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} from './exception-handler.module-definition';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ExceptionHandlerService],
|
||||
exports: [ExceptionHandlerService],
|
||||
})
|
||||
export class ExceptionHandlerModule extends ConfigurableModuleClass {
|
||||
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue:
|
||||
options.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(options.options),
|
||||
};
|
||||
const dynamicModule = super.forRoot(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options?.useFactory?.(...args);
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(config.options);
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
const dynamicModule = super.forRootAsync(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||
|
||||
import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants';
|
||||
|
||||
describe('ExceptionHandlerService', () => {
|
||||
let service: ExceptionHandlerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ExceptionHandlerService,
|
||||
{
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ExceptionHandlerService>(ExceptionHandlerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||
|
||||
import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants';
|
||||
|
||||
@Injectable()
|
||||
export class ExceptionHandlerService {
|
||||
constructor(
|
||||
@Inject(EXCEPTION_HANDLER_DRIVER)
|
||||
private driver: ExceptionHandlerDriverInterface,
|
||||
) {}
|
||||
|
||||
captureException(exception: unknown) {
|
||||
this.driver.captureException(exception);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface ExceptionHandlerDriverInterface {
|
||||
captureException(exception: unknown): void;
|
||||
captureMessage(message: string): void;
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
export enum ExceptionHandlerDriver {
|
||||
Sentry = 'sentry',
|
||||
Console = 'console',
|
||||
}
|
||||
|
||||
export interface ExceptionHandlerSentryDriverFactoryOptions {
|
||||
type: ExceptionHandlerDriver.Sentry;
|
||||
options: {
|
||||
dns: string;
|
||||
serverInstance: Router;
|
||||
debug?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExceptionHandlerDriverFactoryOptions {
|
||||
type: ExceptionHandlerDriver.Console;
|
||||
}
|
||||
|
||||
export type ExceptionHandlerModuleOptions =
|
||||
| ExceptionHandlerSentryDriverFactoryOptions
|
||||
| ExceptionHandlerDriverFactoryOptions;
|
||||
@ -0,0 +1,2 @@
|
||||
export * from './exception-handler.interface';
|
||||
export * from './exception-handler-driver.interface';
|
||||
@ -0,0 +1,11 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export interface StorageDriver {
|
||||
read(params: { folderPath: string; filename: string }): Promise<Readable>;
|
||||
write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void>;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
storagePath: string;
|
||||
}
|
||||
|
||||
export class LocalDriver implements StorageDriver {
|
||||
private options: LocalDriverOptions;
|
||||
|
||||
constructor(options: LocalDriverOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async createFolder(path: string) {
|
||||
if (existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fs.mkdir(path, { recursive: true });
|
||||
}
|
||||
|
||||
async write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const filePath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.folder,
|
||||
params.name,
|
||||
);
|
||||
const folderPath = dirname(filePath);
|
||||
|
||||
await this.createFolder(folderPath);
|
||||
|
||||
await fs.writeFile(filePath, params.file);
|
||||
}
|
||||
|
||||
async read(params: {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
}): Promise<Readable> {
|
||||
const filePath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.folderPath,
|
||||
params.filename,
|
||||
);
|
||||
|
||||
return createReadStream(filePath);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import {
|
||||
CreateBucketCommandInput,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommandInput,
|
||||
NotFound,
|
||||
PutObjectCommand,
|
||||
S3,
|
||||
S3ClientConfig,
|
||||
} from '@aws-sdk/client-s3';
|
||||
|
||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||
|
||||
export interface S3DriverOptions extends S3ClientConfig {
|
||||
bucketName: string;
|
||||
endpoint?: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
export class S3Driver implements StorageDriver {
|
||||
private s3Client: S3;
|
||||
private bucketName: string;
|
||||
|
||||
constructor(options: S3DriverOptions) {
|
||||
const { bucketName, region, endpoint, ...s3Options } = options;
|
||||
|
||||
if (!bucketName || !region) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.s3Client = new S3({ ...s3Options, region, endpoint });
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public get client(): S3 {
|
||||
return this.s3Client;
|
||||
}
|
||||
|
||||
async write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const command = new PutObjectCommand({
|
||||
Key: `${params.folder}/${params.name}`,
|
||||
Body: params.file,
|
||||
ContentType: params.mimeType,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
}
|
||||
|
||||
async read(params: {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
}): Promise<Readable> {
|
||||
const command = new GetObjectCommand({
|
||||
Key: `${params.folderPath}/${params.filename}`,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
const file = await this.s3Client.send(command);
|
||||
|
||||
if (!file || !file.Body || !(file.Body instanceof Readable)) {
|
||||
throw new Error('Unable to get file stream');
|
||||
}
|
||||
|
||||
return Readable.from(file.Body);
|
||||
}
|
||||
|
||||
async checkBucketExists(args: HeadBucketCommandInput) {
|
||||
try {
|
||||
await this.s3Client.headBucket(args);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFound) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createBucket(args: CreateBucketCommandInput) {
|
||||
const exist = await this.checkBucketExists({
|
||||
Bucket: args.Bucket,
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.s3Client.createBucket(args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const STORAGE_DRIVER = Symbol('STORAGE_DRIVER');
|
||||
@ -0,0 +1,14 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
import { FileStorageModuleOptions } from './interfaces';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<FileStorageModuleOptions>({
|
||||
moduleName: 'FileStorage',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
@ -0,0 +1,53 @@
|
||||
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
||||
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import {
|
||||
FileStorageModuleOptions,
|
||||
StorageDriverType,
|
||||
} from 'src/integrations/file-storage/interfaces';
|
||||
|
||||
/**
|
||||
* FileStorage Module factory
|
||||
* @param environment
|
||||
* @returns FileStorageModuleOptions
|
||||
*/
|
||||
export const fileStorageModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
): Promise<FileStorageModuleOptions> => {
|
||||
const driverType = environmentService.getStorageDriverType();
|
||||
|
||||
switch (driverType) {
|
||||
case StorageDriverType.Local: {
|
||||
const storagePath = environmentService.getStorageLocalPath();
|
||||
|
||||
return {
|
||||
type: StorageDriverType.Local,
|
||||
options: {
|
||||
storagePath: process.cwd() + '/' + storagePath,
|
||||
},
|
||||
};
|
||||
}
|
||||
case StorageDriverType.S3: {
|
||||
const bucketName = environmentService.getStorageS3Name();
|
||||
const endpoint = environmentService.getStorageS3Endpoint();
|
||||
const region = environmentService.getStorageS3Region();
|
||||
|
||||
return {
|
||||
type: StorageDriverType.S3,
|
||||
options: {
|
||||
bucketName: bucketName ?? '',
|
||||
endpoint: endpoint,
|
||||
credentials: fromNodeProviderChain({
|
||||
clientConfig: { region },
|
||||
}),
|
||||
forcePathStyle: true,
|
||||
region: region ?? '',
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid storage driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import {
|
||||
FileStorageModuleAsyncOptions,
|
||||
FileStorageModuleOptions,
|
||||
} from './interfaces';
|
||||
import { STORAGE_DRIVER } from './file-storage.constants';
|
||||
|
||||
import { LocalDriver } from './drivers/local.driver';
|
||||
import { S3Driver } from './drivers/s3.driver';
|
||||
|
||||
@Global()
|
||||
export class FileStorageModule {
|
||||
static forRoot(options: FileStorageModuleOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: STORAGE_DRIVER,
|
||||
useValue:
|
||||
options.type === 's3'
|
||||
? new S3Driver(options.options)
|
||||
: new LocalDriver(options.options),
|
||||
};
|
||||
|
||||
return {
|
||||
module: FileStorageModule,
|
||||
providers: [FileStorageService, provider],
|
||||
exports: [FileStorageService],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: FileStorageModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: STORAGE_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
return config?.type === 's3'
|
||||
? new S3Driver(config.options)
|
||||
: new LocalDriver(config.options);
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
return {
|
||||
module: FileStorageModule,
|
||||
imports: options.imports || [],
|
||||
providers: [FileStorageService, provider],
|
||||
exports: [FileStorageService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { FileStorageService } from './file-storage.service';
|
||||
import { STORAGE_DRIVER } from './file-storage.constants';
|
||||
|
||||
describe('FileStorageService', () => {
|
||||
let service: FileStorageService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
FileStorageService,
|
||||
{
|
||||
provide: STORAGE_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FileStorageService>(FileStorageService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { STORAGE_DRIVER } from './file-storage.constants';
|
||||
|
||||
import { StorageDriver } from './drivers/interfaces/storage-driver.interface';
|
||||
|
||||
@Injectable()
|
||||
export class FileStorageService implements StorageDriver {
|
||||
constructor(@Inject(STORAGE_DRIVER) private driver: StorageDriver) {}
|
||||
|
||||
write(params: {
|
||||
file: string | Buffer | Uint8Array;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
return this.driver.write(params);
|
||||
}
|
||||
|
||||
read(params: { folderPath: string; filename: string }): Promise<Readable> {
|
||||
return this.driver.read(params);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { S3DriverOptions } from 'src/integrations/file-storage/drivers/s3.driver';
|
||||
import { LocalDriverOptions } from 'src/integrations/file-storage/drivers/local.driver';
|
||||
|
||||
export enum StorageDriverType {
|
||||
S3 = 's3',
|
||||
Local = 'local',
|
||||
}
|
||||
|
||||
export interface S3DriverFactoryOptions {
|
||||
type: StorageDriverType.S3;
|
||||
options: S3DriverOptions;
|
||||
}
|
||||
|
||||
export interface LocalDriverFactoryOptions {
|
||||
type: StorageDriverType.Local;
|
||||
options: LocalDriverOptions;
|
||||
}
|
||||
|
||||
export type FileStorageModuleOptions =
|
||||
| S3DriverFactoryOptions
|
||||
| LocalDriverFactoryOptions;
|
||||
|
||||
export type FileStorageModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => FileStorageModuleOptions | Promise<FileStorageModuleOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1 @@
|
||||
export * from './file-storage.interface';
|
||||
@ -0,0 +1,39 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
|
||||
import { ExceptionHandlerModule } from 'src/integrations/exception-handler/exception-handler.module';
|
||||
import { exceptionHandlerModuleFactory } from 'src/integrations/exception-handler/exception-handler.module-factory';
|
||||
import { fileStorageModuleFactory } from 'src/integrations/file-storage/file-storage.module-factory';
|
||||
import { loggerModuleFactory } from 'src/integrations/logger/logger.module-factory';
|
||||
import { messageQueueModuleFactory } from 'src/integrations/message-queue/message-queue.module-factory';
|
||||
|
||||
import { EnvironmentModule } from './environment/environment.module';
|
||||
import { EnvironmentService } from './environment/environment.service';
|
||||
import { FileStorageModule } from './file-storage/file-storage.module';
|
||||
import { LoggerModule } from './logger/logger.module';
|
||||
import { MessageQueueModule } from './message-queue/message-queue.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
EnvironmentModule.forRoot({}),
|
||||
FileStorageModule.forRootAsync({
|
||||
useFactory: fileStorageModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
LoggerModule.forRootAsync({
|
||||
useFactory: loggerModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
MessageQueueModule.forRoot({
|
||||
useFactory: messageQueueModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
ExceptionHandlerModule.forRootAsync({
|
||||
useFactory: exceptionHandlerModuleFactory,
|
||||
inject: [EnvironmentService, HttpAdapterHost],
|
||||
}),
|
||||
],
|
||||
exports: [],
|
||||
providers: [],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
@ -0,0 +1 @@
|
||||
export * from './logger.interface';
|
||||
@ -0,0 +1,9 @@
|
||||
export enum LoggerDriverType {
|
||||
Console = 'console',
|
||||
}
|
||||
|
||||
export interface ConsoleDriverFactoryOptions {
|
||||
type: LoggerDriverType.Console;
|
||||
}
|
||||
|
||||
export type LoggerModuleOptions = ConsoleDriverFactoryOptions;
|
||||
@ -0,0 +1 @@
|
||||
export const LOGGER_DRIVER = Symbol('LOGGER_DRIVER');
|
||||
@ -0,0 +1,25 @@
|
||||
import {
|
||||
ConfigurableModuleBuilder,
|
||||
FactoryProvider,
|
||||
ModuleMetadata,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { LoggerModuleOptions } from './interfaces';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<LoggerModuleOptions>({
|
||||
moduleName: 'LoggerService',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
|
||||
export type LoggerModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => LoggerModuleOptions | Promise<LoggerModuleOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1,28 @@
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import {
|
||||
LoggerModuleOptions,
|
||||
LoggerDriverType,
|
||||
} from 'src/integrations/logger/interfaces';
|
||||
|
||||
/**
|
||||
* Logger Module factory
|
||||
* @param environment
|
||||
* @returns LoggerModuleOptions
|
||||
*/
|
||||
export const loggerModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
): Promise<LoggerModuleOptions> => {
|
||||
const driverType = environmentService.getLoggerDriverType();
|
||||
|
||||
switch (driverType) {
|
||||
case LoggerDriverType.Console: {
|
||||
return {
|
||||
type: LoggerDriverType.Console,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid logger driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,58 @@
|
||||
import { DynamicModule, Global, ConsoleLogger, Module } from '@nestjs/common';
|
||||
|
||||
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
|
||||
|
||||
import { LoggerService } from './logger.service';
|
||||
import { LOGGER_DRIVER } from './logger.constants';
|
||||
import {
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
ConfigurableModuleClass,
|
||||
OPTIONS_TYPE,
|
||||
} from './logger.module-definition';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LoggerService],
|
||||
exports: [LoggerService],
|
||||
})
|
||||
export class LoggerModule extends ConfigurableModuleClass {
|
||||
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: LOGGER_DRIVER,
|
||||
useValue:
|
||||
options.type === LoggerDriverType.Console
|
||||
? new ConsoleLogger()
|
||||
: undefined,
|
||||
};
|
||||
const dynamicModule = super.forRoot(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: LOGGER_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options?.useFactory?.(...args);
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config?.type === LoggerDriverType.Console
|
||||
? new ConsoleLogger()
|
||||
: undefined;
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
const dynamicModule = super.forRootAsync(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from './logger.service';
|
||||
import { LOGGER_DRIVER } from './logger.constants';
|
||||
|
||||
describe('LoggerService', () => {
|
||||
let service: LoggerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoggerService,
|
||||
{
|
||||
provide: LOGGER_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoggerService>(LoggerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
LogLevel,
|
||||
LoggerService as LoggerServiceInterface,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { LOGGER_DRIVER } from './logger.constants';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerService implements LoggerServiceInterface {
|
||||
constructor(@Inject(LOGGER_DRIVER) private driver: LoggerServiceInterface) {}
|
||||
|
||||
log(message: any, category: string, ...optionalParams: any[]) {
|
||||
this.driver.log.apply(this.driver, [message, category, ...optionalParams]);
|
||||
}
|
||||
|
||||
error(message: any, category: string, ...optionalParams: any[]) {
|
||||
this.driver.error.apply(this.driver, [
|
||||
message,
|
||||
category,
|
||||
...optionalParams,
|
||||
]);
|
||||
}
|
||||
|
||||
warn(message: any, category: string, ...optionalParams: any[]) {
|
||||
this.driver.warn.apply(this.driver, [message, category, ...optionalParams]);
|
||||
}
|
||||
|
||||
debug?(message: any, category: string, ...optionalParams: any[]) {
|
||||
this.driver.debug?.apply(this.driver, [
|
||||
message,
|
||||
category,
|
||||
...optionalParams,
|
||||
]);
|
||||
}
|
||||
|
||||
verbose?(message: any, category: string, ...optionalParams: any[]) {
|
||||
this.driver.verbose?.apply(this.driver, [
|
||||
message,
|
||||
category,
|
||||
...optionalParams,
|
||||
]);
|
||||
}
|
||||
|
||||
setLogLevels(levels: LogLevel[]) {
|
||||
this.driver.setLogLevels?.apply(this.driver, [levels]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util';
|
||||
|
||||
export const InjectMemoryStorage = (identifier: string) => {
|
||||
const injectionToken = createMemoryStorageInjectionToken(identifier);
|
||||
|
||||
return Inject(injectionToken);
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
export interface MemoryStorageDriver<T> {
|
||||
read(params: { key: string }): Promise<T | null>;
|
||||
write(params: { key: string; data: T }): Promise<void>;
|
||||
delete(params: { key: string }): Promise<void>;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
import { MemoryStorageDriver } from './interfaces/memory-storage-driver.interface';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface LocalMemoryDriverOptions {}
|
||||
|
||||
export class LocalMemoryDriver<T> implements MemoryStorageDriver<T> {
|
||||
private identifier: string;
|
||||
private options: LocalMemoryDriverOptions;
|
||||
private serializer: MemoryStorageSerializer<T>;
|
||||
private storage: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
identifier: string,
|
||||
options: LocalMemoryDriverOptions,
|
||||
serializer: MemoryStorageSerializer<T>,
|
||||
) {
|
||||
this.identifier = identifier;
|
||||
this.options = options;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
async write(params: { key: string; data: T }): Promise<void> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
const serializedData = this.serializer.serialize(params.data);
|
||||
|
||||
this.storage.set(compositeKey, serializedData);
|
||||
}
|
||||
|
||||
async read(params: { key: string }): Promise<T | null> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
|
||||
if (!this.storage.has(compositeKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = this.storage.get(compositeKey);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deserializeData = this.serializer.deserialize(data);
|
||||
|
||||
return deserializeData;
|
||||
}
|
||||
|
||||
async delete(params: { key: string }): Promise<void> {
|
||||
const compositeKey = this.generateCompositeKey(params.key);
|
||||
|
||||
if (!this.storage.has(compositeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storage.delete(compositeKey);
|
||||
}
|
||||
|
||||
private generateCompositeKey(key: string): string {
|
||||
return `${this.identifier}:${key}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './memory-storage.interface';
|
||||
@ -0,0 +1,32 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
import { LocalMemoryDriverOptions } from 'src/integrations/memory-storage/drivers/local.driver';
|
||||
|
||||
export enum MemoryStorageDriverType {
|
||||
Local = 'local',
|
||||
}
|
||||
|
||||
export interface LocalMemoryDriverFactoryOptions {
|
||||
type: MemoryStorageDriverType.Local;
|
||||
options: LocalMemoryDriverOptions;
|
||||
}
|
||||
|
||||
interface MemoryStorageModuleBaseOptions {
|
||||
identifier: string;
|
||||
serializer?: MemoryStorageSerializer<any>;
|
||||
}
|
||||
|
||||
export type MemoryStorageModuleOptions = MemoryStorageModuleBaseOptions &
|
||||
LocalMemoryDriverFactoryOptions;
|
||||
|
||||
export type MemoryStorageModuleAsyncOptions = {
|
||||
identifier: string;
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) =>
|
||||
| Omit<MemoryStorageModuleOptions, 'identifier'>
|
||||
| Promise<Omit<MemoryStorageModuleOptions, 'identifier'>>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1 @@
|
||||
export const MEMORY_STORAGE_SERVICE = 'MEMORY_STORAGE_SERVICE';
|
||||
@ -0,0 +1,72 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { MemoryStorageDefaultSerializer } from 'src/integrations/memory-storage/serializers/default.serializer';
|
||||
import { createMemoryStorageInjectionToken } from 'src/integrations/memory-storage/memory-storage.util';
|
||||
|
||||
import {
|
||||
MemoryStorageDriverType,
|
||||
MemoryStorageModuleAsyncOptions,
|
||||
MemoryStorageModuleOptions,
|
||||
} from './interfaces';
|
||||
|
||||
import { LocalMemoryDriver } from './drivers/local.driver';
|
||||
|
||||
@Global()
|
||||
export class MemoryStorageModule {
|
||||
static forRoot(options: MemoryStorageModuleOptions): DynamicModule {
|
||||
// Dynamic injection token to allow multiple instances of the same driver
|
||||
const injectionToken = createMemoryStorageInjectionToken(
|
||||
options.identifier,
|
||||
);
|
||||
const provider = {
|
||||
provide: injectionToken,
|
||||
useValue: this.createStorageDriver(options),
|
||||
};
|
||||
|
||||
return {
|
||||
module: MemoryStorageModule,
|
||||
providers: [provider],
|
||||
exports: [provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: MemoryStorageModuleAsyncOptions): DynamicModule {
|
||||
// Dynamic injection token to allow multiple instances of the same driver
|
||||
const injectionToken = createMemoryStorageInjectionToken(
|
||||
options.identifier,
|
||||
);
|
||||
const provider = {
|
||||
provide: injectionToken,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
return this.createStorageDriver({
|
||||
identifier: options.identifier,
|
||||
...config,
|
||||
});
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
return {
|
||||
module: MemoryStorageModule,
|
||||
imports: options.imports || [],
|
||||
providers: [provider],
|
||||
exports: [provider],
|
||||
};
|
||||
}
|
||||
|
||||
private static createStorageDriver(options: MemoryStorageModuleOptions) {
|
||||
switch (options.type) {
|
||||
case MemoryStorageDriverType.Local:
|
||||
return new LocalMemoryDriver(
|
||||
options.identifier,
|
||||
options.options,
|
||||
options.serializer ?? new MemoryStorageDefaultSerializer<string>(),
|
||||
);
|
||||
// Future case for Redis or other types
|
||||
default:
|
||||
throw new Error(`Unsupported storage type: ${options.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MemoryStorageService } from './memory-storage.service';
|
||||
|
||||
describe('MemoryStorageService', () => {
|
||||
let service: MemoryStorageService<any>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [MemoryStorageService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MemoryStorageService<any>>(MemoryStorageService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { MemoryStorageDriver } from 'src/integrations/memory-storage/drivers/interfaces/memory-storage-driver.interface';
|
||||
|
||||
export class MemoryStorageService<T> implements MemoryStorageDriver<T> {
|
||||
private driver: MemoryStorageDriver<T>;
|
||||
|
||||
constructor(driver: MemoryStorageDriver<T>) {
|
||||
this.driver = driver;
|
||||
}
|
||||
|
||||
write(params: { key: string; data: T }): Promise<void> {
|
||||
return this.driver.write(params);
|
||||
}
|
||||
|
||||
read(params: { key: string }): Promise<T | null> {
|
||||
return this.driver.read(params);
|
||||
}
|
||||
|
||||
delete(params: { key: string }): Promise<void> {
|
||||
return this.driver.delete(params);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { MEMORY_STORAGE_SERVICE } from 'src/integrations/memory-storage/memory-storage.constants';
|
||||
|
||||
export const createMemoryStorageInjectionToken = (identifier: string) => {
|
||||
return `${MEMORY_STORAGE_SERVICE}_${identifier}`;
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
export class MemoryStorageDefaultSerializer<T>
|
||||
implements MemoryStorageSerializer<T>
|
||||
{
|
||||
serialize(item: T): string {
|
||||
if (typeof item !== 'string') {
|
||||
throw new Error('DefaultSerializer can only serialize strings');
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
deserialize(data: string): T {
|
||||
return data as unknown as T;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface MemoryStorageSerializer<T> {
|
||||
serialize(item: T): string;
|
||||
deserialize(data: string): T;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { MemoryStorageSerializer } from 'src/integrations/memory-storage/serializers/interfaces/memory-storage-serializer.interface';
|
||||
|
||||
export class MemoryStorageJsonSerializer<T>
|
||||
implements MemoryStorageSerializer<T>
|
||||
{
|
||||
serialize(item: T): string {
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
deserialize(data: string): T {
|
||||
return JSON.parse(data) as T;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Queue, QueueOptions, Worker } from 'bullmq';
|
||||
|
||||
import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
|
||||
|
||||
import { MessageQueues } from 'src/integrations/message-queue/message-queue.constants';
|
||||
|
||||
import { MessageQueueDriver } from './interfaces/message-queue-driver.interface';
|
||||
|
||||
export type BullMQDriverOptions = QueueOptions;
|
||||
|
||||
export class BullMQDriver implements MessageQueueDriver {
|
||||
private queueMap: Record<MessageQueues, Queue> = {} as Record<
|
||||
MessageQueues,
|
||||
Queue
|
||||
>;
|
||||
private workerMap: Record<MessageQueues, Worker> = {} as Record<
|
||||
MessageQueues,
|
||||
Worker
|
||||
>;
|
||||
|
||||
constructor(private options: BullMQDriverOptions) {}
|
||||
|
||||
register(queueName: MessageQueues): void {
|
||||
this.queueMap[queueName] = new Queue(queueName, this.options);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
const workers = Object.values(this.workerMap);
|
||||
const queues = Object.values(this.queueMap);
|
||||
|
||||
await Promise.all([
|
||||
...queues.map((q) => q.close()),
|
||||
...workers.map((w) => w.close()),
|
||||
]);
|
||||
}
|
||||
|
||||
async work<T>(
|
||||
queueName: MessageQueues,
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void>,
|
||||
) {
|
||||
const worker = new Worker(queueName, async (job) => {
|
||||
await handler(job as { data: T; id: string });
|
||||
});
|
||||
|
||||
this.workerMap[queueName] = worker;
|
||||
}
|
||||
|
||||
async add<T>(
|
||||
queueName: MessageQueues,
|
||||
data: T,
|
||||
options?: QueueJobOptions,
|
||||
): Promise<void> {
|
||||
if (!this.queueMap[queueName]) {
|
||||
throw new Error(
|
||||
`Queue ${queueName} is not registered, make sure you have added it as a queue provider`,
|
||||
);
|
||||
}
|
||||
await this.queueMap[queueName].add(options?.id || '', data, {
|
||||
priority: options?.priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export interface QueueJobOptions {
|
||||
id?: string;
|
||||
priority?: number;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
|
||||
|
||||
import { MessageQueues } from 'src/integrations/message-queue/message-queue.constants';
|
||||
|
||||
export interface MessageQueueDriver {
|
||||
add<T>(
|
||||
queueName: MessageQueues,
|
||||
data: T,
|
||||
options?: QueueJobOptions,
|
||||
): Promise<void>;
|
||||
work<T>(
|
||||
queueName: string,
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void> | void,
|
||||
);
|
||||
stop?(): Promise<void>;
|
||||
register?(queueName: MessageQueues): void;
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import PgBoss from 'pg-boss';
|
||||
|
||||
import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
|
||||
|
||||
import { MessageQueueDriver } from './interfaces/message-queue-driver.interface';
|
||||
|
||||
export type PgBossDriverOptions = PgBoss.ConstructorOptions;
|
||||
|
||||
export class PgBossDriver implements MessageQueueDriver {
|
||||
private pgBoss: PgBoss;
|
||||
|
||||
constructor(options: PgBossDriverOptions) {
|
||||
this.pgBoss = new PgBoss(options);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.pgBoss.stop();
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.pgBoss.start();
|
||||
}
|
||||
|
||||
async work<T>(
|
||||
queueName: string,
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void>,
|
||||
) {
|
||||
return this.pgBoss.work(queueName, handler);
|
||||
}
|
||||
|
||||
async add<T>(
|
||||
queueName: string,
|
||||
data: T,
|
||||
options?: QueueJobOptions,
|
||||
): Promise<void> {
|
||||
await this.pgBoss.send(queueName, data as object, options ? options : {});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './message-queue.interface';
|
||||
@ -0,0 +1,30 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { BullMQDriverOptions } from 'src/integrations/message-queue/drivers/bullmq.driver';
|
||||
import { PgBossDriverOptions } from 'src/integrations/message-queue/drivers/pg-boss.driver';
|
||||
|
||||
export enum MessageQueueDriverType {
|
||||
PgBoss = 'pg-boss',
|
||||
BullMQ = 'bull-mq',
|
||||
}
|
||||
|
||||
export interface PgBossDriverFactoryOptions {
|
||||
type: MessageQueueDriverType.PgBoss;
|
||||
options: PgBossDriverOptions;
|
||||
}
|
||||
|
||||
export interface BullMQDriverFactoryOptions {
|
||||
type: MessageQueueDriverType.BullMQ;
|
||||
options: BullMQDriverOptions;
|
||||
}
|
||||
|
||||
export type MessageQueueModuleOptions =
|
||||
| PgBossDriverFactoryOptions
|
||||
| BullMQDriverFactoryOptions;
|
||||
|
||||
export type MessageQueueModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => MessageQueueModuleOptions | Promise<MessageQueueModuleOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1,5 @@
|
||||
export const QUEUE_DRIVER = Symbol('QUEUE_DRIVER');
|
||||
|
||||
export enum MessageQueues {
|
||||
taskAssignedQueue = 'task-assigned-queue',
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import {
|
||||
MessageQueueDriverType,
|
||||
MessageQueueModuleOptions,
|
||||
} from 'src/integrations/message-queue/interfaces';
|
||||
|
||||
/**
|
||||
* MessageQueue Module factory
|
||||
* @param environment
|
||||
* @returns MessageQueueModuleOptions
|
||||
*/
|
||||
export const messageQueueModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
): Promise<MessageQueueModuleOptions> => {
|
||||
const driverType = environmentService.getMessageQueueDriverType();
|
||||
|
||||
switch (driverType) {
|
||||
case MessageQueueDriverType.PgBoss: {
|
||||
const connectionString = environmentService.getPGDatabaseUrl();
|
||||
|
||||
return {
|
||||
type: MessageQueueDriverType.PgBoss,
|
||||
options: {
|
||||
connectionString,
|
||||
},
|
||||
};
|
||||
}
|
||||
case MessageQueueDriverType.BullMQ: {
|
||||
const host = environmentService.getRedisHost();
|
||||
const port = environmentService.getRedisPort();
|
||||
|
||||
return {
|
||||
type: MessageQueueDriverType.BullMQ,
|
||||
options: {
|
||||
connection: {
|
||||
host,
|
||||
port,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid message queue driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
|
||||
|
||||
import {
|
||||
MessageQueueDriverType,
|
||||
MessageQueueModuleAsyncOptions,
|
||||
} from 'src/integrations/message-queue/interfaces';
|
||||
import {
|
||||
QUEUE_DRIVER,
|
||||
MessageQueues,
|
||||
} from 'src/integrations/message-queue/message-queue.constants';
|
||||
import { PgBossDriver } from 'src/integrations/message-queue/drivers/pg-boss.driver';
|
||||
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
|
||||
import { BullMQDriver } from 'src/integrations/message-queue/drivers/bullmq.driver';
|
||||
|
||||
@Global()
|
||||
export class MessageQueueModule {
|
||||
static forRoot(options: MessageQueueModuleAsyncOptions): DynamicModule {
|
||||
const providers = [
|
||||
{
|
||||
provide: MessageQueues.taskAssignedQueue,
|
||||
useFactory: (driver: MessageQueueDriver) => {
|
||||
return new MessageQueueService(
|
||||
driver,
|
||||
MessageQueues.taskAssignedQueue,
|
||||
);
|
||||
},
|
||||
inject: [QUEUE_DRIVER],
|
||||
},
|
||||
{
|
||||
provide: QUEUE_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
if (config.type === MessageQueueDriverType.PgBoss) {
|
||||
const boss = new PgBossDriver(config.options);
|
||||
|
||||
await boss.init();
|
||||
|
||||
return boss;
|
||||
}
|
||||
|
||||
return new BullMQDriver(config.options);
|
||||
},
|
||||
inject: options.inject || [],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
module: MessageQueueModule,
|
||||
imports: options.imports || [],
|
||||
providers,
|
||||
exports: [MessageQueues.taskAssignedQueue],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
|
||||
|
||||
import {
|
||||
QUEUE_DRIVER,
|
||||
MessageQueues,
|
||||
} from 'src/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
|
||||
|
||||
describe('MessageQueueTaskAssigned queue', () => {
|
||||
let service: MessageQueueService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: MessageQueues.taskAssignedQueue,
|
||||
useFactory: (driver: MessageQueueDriver) => {
|
||||
return new MessageQueueService(
|
||||
driver,
|
||||
MessageQueues.taskAssignedQueue,
|
||||
);
|
||||
},
|
||||
inject: [QUEUE_DRIVER],
|
||||
},
|
||||
{
|
||||
provide: QUEUE_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MessageQueueService>(MessageQueues.taskAssignedQueue);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
it('should contain the topic and driver', () => {
|
||||
expect(service).toEqual({
|
||||
driver: {},
|
||||
queueName: MessageQueues.taskAssignedQueue,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,37 @@
|
||||
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
|
||||
import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
|
||||
import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
|
||||
|
||||
import {
|
||||
MessageQueues,
|
||||
QUEUE_DRIVER,
|
||||
} from 'src/integrations/message-queue/message-queue.constants';
|
||||
|
||||
@Injectable()
|
||||
export class MessageQueueService implements OnModuleDestroy {
|
||||
constructor(
|
||||
@Inject(QUEUE_DRIVER) protected driver: MessageQueueDriver,
|
||||
protected queueName: MessageQueues,
|
||||
) {
|
||||
if (typeof this.driver.register === 'function') {
|
||||
this.driver.register(queueName);
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (typeof this.driver.stop === 'function') {
|
||||
await this.driver.stop();
|
||||
}
|
||||
}
|
||||
|
||||
add<T>(data: T, options?: QueueJobOptions): Promise<void> {
|
||||
return this.driver.add(this.queueName, data, options);
|
||||
}
|
||||
|
||||
work<T>(
|
||||
handler: ({ data, id }: { data: T; id: string }) => Promise<void> | void,
|
||||
) {
|
||||
return this.driver.work(this.queueName, handler);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user