feat: exceptions handlers (#2855)

* feat: wip exception handlers

* feat: exception capturer

* fix: rename exception-capturer into exception-handler

* fix: remove unused variable
This commit is contained in:
Jérémy M
2023-12-08 10:18:50 +01:00
committed by GitHub
parent 6c83953633
commit cf334ada0e
38 changed files with 983 additions and 340 deletions

View File

@ -10,6 +10,9 @@ import { ExtractJwt } from 'passport-jwt';
import { TokenExpiredError, JsonWebTokenError, verify } from 'jsonwebtoken';
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter';
import { HttpExceptionFilter } from 'src/filters/http-exception.filter';
import { GlobalExceptionFilter } from 'src/filters/global-exception.filter';
import { AppService } from './app.service';
@ -22,7 +25,6 @@ import {
JwtAuthStrategy,
JwtPayload,
} from './core/auth/strategies/jwt.auth.strategy';
import { ExceptionFilter } from './filters/exception.filter';
@Module({
imports: [
@ -111,9 +113,20 @@ import { ExceptionFilter } from './filters/exception.filter';
],
providers: [
AppService,
// Exceptions filters must be ordered from the least specific to the most specific
// If TypeOrmExceptionFilter handle something, HttpExceptionFilter will not handle it
// GlobalExceptionFilter will handle the rest of the exceptions
{
provide: APP_FILTER,
useClass: ExceptionFilter,
useClass: GlobalExceptionFilter,
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
{
provide: APP_FILTER,
useClass: TypeOrmExceptionFilter,
},
],
})

View File

@ -1,56 +0,0 @@
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
import { TypeORMError } from 'typeorm';
import {
AuthenticationError,
BaseGraphQLError,
ForbiddenError,
} from 'src/filters/utils/graphql-errors.util';
const graphQLPredefinedExceptions = {
401: AuthenticationError,
403: ForbiddenError,
};
@Catch()
export class ExceptionFilter implements GqlExceptionFilter {
catch(exception: HttpException | TypeORMError, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
return null;
}
if (exception instanceof TypeORMError) {
const error = new BaseGraphQLError(
exception.name,
'INTERNAL_SERVER_ERROR',
);
error.stack = exception.stack;
error.extensions['response'] = exception.message;
return error;
} else if (exception instanceof HttpException) {
let error: BaseGraphQLError;
if (exception.getStatus() in graphQLPredefinedExceptions) {
error = new graphQLPredefinedExceptions[exception.getStatus()](
exception.message,
);
} else {
error = new BaseGraphQLError(
exception.message,
exception.getStatus().toString(),
);
}
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
return error;
}
return exception;
}
}

View File

@ -0,0 +1,18 @@
import { Catch, Injectable } from '@nestjs/common';
import { GqlExceptionFilter } from '@nestjs/graphql';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
@Catch()
@Injectable()
export class GlobalExceptionFilter implements GqlExceptionFilter {
constructor(
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
catch(exception: unknown) {
this.exceptionHandlerService.captureException(exception);
return exception;
}
}

View File

@ -0,0 +1,42 @@
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
import {
AuthenticationError,
BaseGraphQLError,
ForbiddenError,
} from 'src/filters/utils/graphql-errors.util';
const graphQLPredefinedExceptions = {
401: AuthenticationError,
403: ForbiddenError,
};
@Catch(HttpException)
export class HttpExceptionFilter
implements GqlExceptionFilter<HttpException, BaseGraphQLError | null>
{
catch(exception: HttpException, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
return null;
}
let error: BaseGraphQLError;
if (exception.getStatus() in graphQLPredefinedExceptions) {
error = new graphQLPredefinedExceptions[exception.getStatus()](
exception.message,
);
} else {
error = new BaseGraphQLError(
exception.message,
exception.getStatus().toString(),
);
}
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
return error;
}
}

View File

@ -0,0 +1,24 @@
import { ArgumentsHost, Catch } from '@nestjs/common';
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
import { TypeORMError } from 'typeorm';
import { BaseGraphQLError } from 'src/filters/utils/graphql-errors.util';
@Catch(TypeORMError)
export class TypeOrmExceptionFilter
implements GqlExceptionFilter<TypeORMError, BaseGraphQLError | null>
{
catch(exception: TypeORMError, host: ArgumentsHost) {
if (host.getType<GqlContextType>() !== 'graphql') {
return null;
}
const error = new BaseGraphQLError(exception.name, 'INTERNAL_SERVER_ERROR');
error.stack = exception.stack;
error.extensions['response'] = exception.message;
return error;
}
}

View File

@ -2,11 +2,13 @@
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 { StorageType } from './interfaces/storage.interface';
import { SupportDriver } from './interfaces/support.interface';
import { LoggerDriver } from './interfaces/logger.interface';
import { MessageQueueType } from './interfaces/message-queue.interface';
@Injectable()
export class EnvironmentService {
@ -109,16 +111,17 @@ export class EnvironmentService {
return this.configService.get<string>('AUTH_GOOGLE_CALLBACK_URL');
}
getStorageType(): StorageType {
getStorageDriverType(): StorageDriverType {
return (
this.configService.get<StorageType>('STORAGE_TYPE') ?? StorageType.Local
this.configService.get<StorageDriverType>('STORAGE_TYPE') ??
StorageDriverType.Local
);
}
getMessageQueueType(): MessageQueueType {
getMessageQueueDriverType(): MessageQueueDriverType {
return (
this.configService.get<MessageQueueType>('MESSAGE_QUEUE_TYPE') ??
MessageQueueType.PgBoss
this.configService.get<MessageQueueDriverType>('MESSAGE_QUEUE_TYPE') ??
MessageQueueDriverType.PgBoss
);
}
@ -154,9 +157,18 @@ export class EnvironmentService {
return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY');
}
getLoggerDriver(): string {
getLoggerDriverType(): LoggerDriverType {
return (
this.configService.get<string>('LOGGER_DRIVER') ?? LoggerDriver.Console
this.configService.get<LoggerDriverType>('LOGGER_DRIVER') ??
LoggerDriverType.Console
);
}
getExceptionHandlerDriverType(): ExceptionHandlerDriver {
return (
this.configService.get<ExceptionHandlerDriver>(
'EXCEPTION_HANDLER_DRIVER',
) ?? ExceptionHandlerDriver.Console
);
}

View File

@ -14,15 +14,16 @@ import {
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 { StorageType } from './interfaces/storage.interface';
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 { LoggerDriver } from './interfaces/logger.interface';
import { CastToLogLevelArray } from './decorators/cast-to-log-level-array.decorator';
export class EnvironmentVariables {
@ -110,20 +111,20 @@ export class EnvironmentVariables {
AUTH_GOOGLE_CALLBACK_URL?: string;
// Storage
@IsEnum(StorageType)
@IsEnum(StorageDriverType)
@IsOptional()
STORAGE_TYPE?: StorageType;
STORAGE_TYPE?: StorageDriverType;
@ValidateIf((env) => env.STORAGE_TYPE === StorageType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@IsAWSRegion()
STORAGE_S3_REGION?: AwsRegion;
@ValidateIf((env) => env.STORAGE_TYPE === StorageType.S3)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
@IsString()
STORAGE_S3_NAME?: string;
@IsString()
@ValidateIf((env) => env.STORAGE_TYPE === StorageType.Local)
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
STORAGE_LOCAL_PATH?: string;
// Support
@ -139,9 +140,13 @@ export class EnvironmentVariables {
@IsString()
SUPPORT_FRONT_HMAC_KEY?: string;
@IsEnum(LoggerDriver)
@IsEnum(LoggerDriverType)
@IsOptional()
LOGGER_DRIVER?: LoggerDriver;
LOGGER_DRIVER?: LoggerDriverType;
@IsEnum(ExceptionHandlerDriver)
@IsOptional()
EXCEPTION_HANDLER_DRIVER?: ExceptionHandlerDriver;
@CastToLogLevelArray()
@IsOptional()
@ -151,7 +156,9 @@ export class EnvironmentVariables {
@IsOptional()
DEMO_WORKSPACE_IDS?: string[];
@ValidateIf((env) => env.LOGGER_DRIVER === LoggerDriver.Sentry)
@ValidateIf(
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
)
@IsString()
SENTRY_DSN?: string;
}

View File

@ -1,4 +0,0 @@
export enum LoggerDriver {
Console = 'console',
Sentry = 'sentry',
}

View File

@ -1,3 +0,0 @@
export enum MemoryStorageType {
Local = 'local',
}

View File

@ -1,4 +0,0 @@
export enum MessageQueueType {
PgBoss = 'pg-boss',
BullMQ = 'bull-mq',
}

View File

@ -1,4 +0,0 @@
export enum StorageType {
S3 = 's3',
Local = 'local',
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
export const EXCEPTION_HANDLER_DRIVER = Symbol('EXCEPTION_HANDLER_DRIVER');

View File

@ -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'>;

View File

@ -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`,
);
}
};

View 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],
};
}
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
export interface ExceptionHandlerDriverInterface {
captureException(exception: unknown): void;
captureMessage(message: string): void;
}

View File

@ -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;

View File

@ -0,0 +1,2 @@
export * from './exception-handler.interface';
export * from './exception-handler-driver.interface';

View File

@ -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`,
);
}
};

View File

@ -1,17 +1,20 @@
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { StorageType } from 'src/integrations/environment/interfaces/storage.interface';
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: StorageType.S3;
type: StorageDriverType.S3;
options: S3DriverOptions;
}
export interface LocalDriverFactoryOptions {
type: StorageType.Local;
type: StorageDriverType.Local;
options: LocalDriverOptions;
}

View File

@ -1,134 +1,17 @@
import { Module } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
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 { FileStorageModuleOptions } from './file-storage/interfaces';
import { StorageType } from './environment/interfaces/storage.interface';
import { LoggerModule } from './logger/logger.module';
import { LoggerModuleOptions } from './logger/interfaces';
import { LoggerDriver } from './environment/interfaces/logger.interface';
import { MessageQueueModule } from './message-queue/message-queue.module';
import { MessageQueueModuleOptions } from './message-queue/interfaces';
import { MessageQueueType } from './environment/interfaces/message-queue.interface';
/**
* FileStorage Module factory
* @param environment
* @returns FileStorageModuleOptions
*/
const fileStorageModuleFactory = async (
environmentService: EnvironmentService,
): Promise<FileStorageModuleOptions> => {
const type = environmentService.getStorageType();
switch (type) {
case StorageType.Local: {
const storagePath = environmentService.getStorageLocalPath();
return {
type: StorageType.Local,
options: {
storagePath: process.cwd() + '/' + storagePath,
},
};
}
case StorageType.S3: {
const bucketName = environmentService.getStorageS3Name();
const endpoint = environmentService.getStorageS3Endpoint();
const region = environmentService.getStorageS3Region();
return {
type: StorageType.S3,
options: {
bucketName: bucketName ?? '',
endpoint: endpoint,
credentials: fromNodeProviderChain({
clientConfig: { region },
}),
forcePathStyle: true,
region: region ?? '',
},
};
}
default:
throw new Error(`Invalid storage type (${type}), check your .env file`);
}
};
/**
* Logger Module factory
* @param environment
* @returns LoggerModuleOptions
*/
const loggerModuleFactory = async (
environmentService: EnvironmentService,
): Promise<LoggerModuleOptions> => {
const type = environmentService.getLoggerDriver();
switch (type) {
case LoggerDriver.Console: {
return {
type: LoggerDriver.Console,
options: null,
};
}
case LoggerDriver.Sentry: {
return {
type: LoggerDriver.Sentry,
options: {
sentryDNS: environmentService.getSentryDSN() ?? '',
},
};
}
default:
throw new Error(`Invalid logger type (${type}), check your .env file`);
}
};
/**
* MessageQueue Module factory
* @param environment
* @returns MessageQueueModuleOptions
*/
const messageQueueModuleFactory = async (
environmentService: EnvironmentService,
): Promise<MessageQueueModuleOptions> => {
const type = environmentService.getMessageQueueType();
switch (type) {
case MessageQueueType.PgBoss: {
const connectionString = environmentService.getPGDatabaseUrl();
return {
type: MessageQueueType.PgBoss,
options: {
connectionString,
},
};
}
case MessageQueueType.BullMQ: {
const host = environmentService.getRedisHost();
const port = environmentService.getRedisPort();
return {
type: MessageQueueType.BullMQ,
options: {
connection: {
host,
port,
},
},
};
}
default:
throw new Error(
`Invalid message queue type (${type}), check your .env file`,
);
}
};
@Module({
imports: [
@ -145,6 +28,10 @@ const messageQueueModuleFactory = async (
useFactory: messageQueueModuleFactory,
inject: [EnvironmentService],
}),
ExceptionHandlerModule.forRootAsync({
useFactory: exceptionHandlerModuleFactory,
inject: [EnvironmentService, HttpAdapterHost],
}),
],
exports: [],
providers: [],

View File

@ -1,53 +0,0 @@
import { LoggerService } from '@nestjs/common';
import * as Sentry from '@sentry/node';
export interface SentryDriverOptions {
sentryDNS: string;
}
export class SentryDriver implements LoggerService {
constructor(options: SentryDriverOptions) {
Sentry.init({
dsn: options.sentryDNS,
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
}
private logLevels = ['log', 'error', 'warning', 'debug', 'info'];
setLogLevels(levels: string[]) {
this.logLevels = levels;
}
log(message: any) {
if (this.logLevels.includes('log')) {
Sentry.captureMessage(message, { level: 'log' });
}
}
error(message: any) {
if (this.logLevels.includes('error')) {
Sentry.captureMessage(message, { level: 'error' });
}
}
warn(message: any) {
if (this.logLevels.includes('warn')) {
Sentry.captureMessage(message, { level: 'warning' });
}
}
debug?(message: any) {
if (this.logLevels.includes('debug')) {
Sentry.captureMessage(message, { level: 'debug' });
}
}
verbose?(message: any) {
if (this.logLevels.includes('verbose')) {
Sentry.captureMessage(message, { level: 'info' });
}
}
}

View File

@ -1,17 +1,9 @@
import { LoggerDriver } from 'src/integrations/environment/interfaces/logger.interface';
export interface SentryDriverFactoryOptions {
type: LoggerDriver.Sentry;
options: {
sentryDNS: string;
};
export enum LoggerDriverType {
Console = 'console',
}
export interface ConsoleDriverFactoryOptions {
type: LoggerDriver.Console;
options: null;
type: LoggerDriverType.Console;
}
export type LoggerModuleOptions =
| SentryDriverFactoryOptions
| ConsoleDriverFactoryOptions;
export type LoggerModuleOptions = ConsoleDriverFactoryOptions;

View File

@ -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`,
);
}
};

View File

@ -1,50 +1,58 @@
import { DynamicModule, Global, ConsoleLogger } from '@nestjs/common';
import { DynamicModule, Global, ConsoleLogger, Module } from '@nestjs/common';
import { LoggerDriver } from 'src/integrations/environment/interfaces/logger.interface';
import { LoggerDriverType } from 'src/integrations/logger/interfaces';
import { LoggerService } from './logger.service';
import { LoggerModuleOptions } from './interfaces';
import { LOGGER_DRIVER } from './logger.constants';
import { LoggerModuleAsyncOptions } from './logger.module-definition';
import { SentryDriver } from './drivers/sentry.driver';
import {
ASYNC_OPTIONS_TYPE,
ConfigurableModuleClass,
OPTIONS_TYPE,
} from './logger.module-definition';
@Global()
export class LoggerModule {
static forRoot(options: LoggerModuleOptions): DynamicModule {
@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 === LoggerDriver.Console
options.type === LoggerDriverType.Console
? new ConsoleLogger()
: new SentryDriver(options.options),
: undefined,
};
const dynamicModule = super.forRoot(options);
return {
module: LoggerModule,
providers: [LoggerService, provider],
exports: [LoggerService],
...dynamicModule,
providers: [...(dynamicModule.providers ?? []), provider],
};
}
static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule {
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
const provider = {
provide: LOGGER_DRIVER,
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
const config = await options?.useFactory?.(...args);
return config?.type === LoggerDriver.Console
if (!config) {
return null;
}
return config?.type === LoggerDriverType.Console
? new ConsoleLogger()
: new SentryDriver(config.options);
: undefined;
},
inject: options.inject || [],
};
const dynamicModule = super.forRootAsync(options);
return {
module: LoggerModule,
imports: options.imports || [],
providers: [LoggerService, provider],
exports: [LoggerService],
...dynamicModule,
providers: [...(dynamicModule.providers ?? []), provider],
};
}
}

View File

@ -1,12 +1,15 @@
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface';
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: MemoryStorageType.Local;
type: MemoryStorageDriverType.Local;
options: LocalMemoryDriverOptions;
}

View File

@ -1,11 +1,10 @@
import { DynamicModule, Global } from '@nestjs/common';
import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface';
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';
@ -59,7 +58,7 @@ export class MemoryStorageModule {
private static createStorageDriver(options: MemoryStorageModuleOptions) {
switch (options.type) {
case MemoryStorageType.Local:
case MemoryStorageDriverType.Local:
return new LocalMemoryDriver(
options.identifier,
options.options,

View File

@ -1,17 +1,20 @@
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { MessageQueueType } from 'src/integrations/environment/interfaces/message-queue.interface';
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: MessageQueueType.PgBoss;
type: MessageQueueDriverType.PgBoss;
options: PgBossDriverOptions;
}
export interface BullMQDriverFactoryOptions {
type: MessageQueueType.BullMQ;
type: MessageQueueDriverType.BullMQ;
options: BullMQDriverOptions;
}

View File

@ -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`,
);
}
};

View File

@ -1,9 +1,11 @@
import { DynamicModule, Global } from '@nestjs/common';
import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
import { MessageQueueType } from 'src/integrations/environment/interfaces/message-queue.interface';
import { MessageQueueModuleAsyncOptions } from 'src/integrations/message-queue/interfaces';
import {
MessageQueueDriverType,
MessageQueueModuleAsyncOptions,
} from 'src/integrations/message-queue/interfaces';
import {
QUEUE_DRIVER,
MessageQueues,
@ -31,7 +33,7 @@ export class MessageQueueModule {
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
if (config.type === MessageQueueType.PgBoss) {
if (config.type === MessageQueueDriverType.PgBoss) {
const boss = new PgBossDriver(config.options);
await boss.init();

View File

@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { MemoryStorageType } from 'src/integrations/environment/interfaces/memory-storage.interface';
import { MemoryStorageDriverType } from 'src/integrations/memory-storage/interfaces';
import { MemoryStorageModule } from 'src/integrations/memory-storage/memory-storage.module';
import { MemoryStorageJsonSerializer } from 'src/integrations/memory-storage/serializers/json.serializer';
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
@ -15,24 +14,24 @@ import { WorkspaceSchemaStorageService } from 'src/workspace/workspace-schema-st
WorkspaceCacheVersionModule,
MemoryStorageModule.forRoot({
identifier: 'objectMetadataCollection',
type: MemoryStorageType.Local,
type: MemoryStorageDriverType.Local,
options: {},
serializer: new MemoryStorageJsonSerializer<ObjectMetadataEntity[]>(),
}),
MemoryStorageModule.forRoot({
identifier: 'typeDefs',
type: MemoryStorageType.Local,
type: MemoryStorageDriverType.Local,
options: {},
}),
MemoryStorageModule.forRoot({
identifier: 'usedScalarNames',
type: MemoryStorageType.Local,
type: MemoryStorageDriverType.Local,
options: {},
serializer: new MemoryStorageJsonSerializer<string[]>(),
}),
MemoryStorageModule.forRoot({
identifier: 'cacheVersion',
type: MemoryStorageType.Local,
type: MemoryStorageDriverType.Local,
options: {},
}),
],