Skip to content

Commit 524b572

Browse files
committed
feat: decouples from ConfigModule and ConfigService; No more environment variables;
1 parent 3582ca8 commit 524b572

16 files changed

+250
-171
lines changed

src/common.builder.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ConfigurableModuleBuilder } from '@nestjs/common';
2+
import { CommonModuleOptions } from './common.options';
3+
4+
export const { MODULE_OPTIONS_TOKEN, ConfigurableModuleClass } =
5+
new ConfigurableModuleBuilder<CommonModuleOptions>()
6+
.setClassMethodName('forRoot')
7+
.build();

src/common.factory.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { configureContextWrappers } from '@gedai/nestjs-core';
2+
import { NestApplicationOptions } from '@nestjs/common';
3+
import { NestFactory } from '@nestjs/core';
4+
import { configureExceptionHandler } from './logger/exception.handler';
5+
import { configureHttpInspectorInbound } from './logger/http-inspector-inbound.middleware';
6+
import { configureHttpInspectorOutbound } from './logger/http-inspector-outbound.interceptor';
7+
import { configureLogger } from './logger/logger.config';
8+
import { configureCompression } from './utils/compression.config';
9+
import { configureCORS } from './utils/cors.config';
10+
import { configureHelmet } from './utils/helmet.config';
11+
import { configureRoutePrefix } from './utils/route-prefix.config';
12+
import { configureValidation } from './utils/validation.config';
13+
import { configureVersioning } from './utils/versioning.config';
14+
15+
export const createNestApp = async (
16+
appModule: any,
17+
opts?: NestApplicationOptions,
18+
) => {
19+
const { bufferLogs = true } = opts ?? {};
20+
const app = await NestFactory.create(appModule, { ...opts, bufferLogs })
21+
// :: keep layout
22+
.then(configureContextWrappers)
23+
.then(configureLogger)
24+
.then(configureExceptionHandler)
25+
.then(configureHttpInspectorInbound)
26+
.then(configureHttpInspectorOutbound)
27+
.then(configureCORS)
28+
.then(configureHelmet)
29+
.then(configureCompression)
30+
.then(configureValidation)
31+
.then(configureVersioning)
32+
.then(configureRoutePrefix);
33+
34+
return app;
35+
};

src/common.module.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Global, Module } from '@nestjs/common';
2+
import {
3+
ConfigurableModuleClass,
4+
MODULE_OPTIONS_TOKEN,
5+
} from './common.builder';
6+
7+
@Global()
8+
@Module({ exports: [MODULE_OPTIONS_TOKEN] })
9+
export class CommonModule extends ConfigurableModuleClass {}

src/common.options.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ValidationPipeOptions, VersioningOptions } from '@nestjs/common';
2+
import {
3+
CorsOptions,
4+
CorsOptionsDelegate,
5+
} from '@nestjs/common/interfaces/external/cors-options.interface';
6+
import * as compression from 'compression';
7+
import { HelmetOptions } from 'helmet';
8+
import { Obfuscator } from './logger/obfuscator';
9+
10+
export type ObfuscationOptions = {
11+
obfuscator?: Obfuscator;
12+
sensitiveKeys?: (string | RegExp)[];
13+
};
14+
15+
export type LoggerOptions = {
16+
silent?: boolean;
17+
level?: 'debug' | 'verbose' | 'info' | 'warn' | 'error';
18+
format?: 'json' | 'pretty';
19+
obfuscation?: ObfuscationOptions;
20+
};
21+
22+
export type HttpTrafficInspectionOptions = {
23+
mode?: 'none' | 'all' | 'inbound' | 'outbound';
24+
ignoreRoutes: string[];
25+
};
26+
27+
export type CORSOption = CorsOptions | CorsOptionsDelegate<any>;
28+
29+
export type CommonModuleOptions = {
30+
appName?: string;
31+
environment?: string;
32+
httpTrafficInspection?: HttpTrafficInspectionOptions;
33+
logger?: LoggerOptions;
34+
cors?: CORSOption;
35+
helmet?: Readonly<HelmetOptions>;
36+
compression?: compression.CompressionOptions;
37+
validationPipe?: ValidationPipeOptions;
38+
versioning?: VersioningOptions;
39+
routePrefix?: string;
40+
};
41+
42+
export interface CommonOptionsFactory {
43+
createOptions(): CommonModuleOptions | Promise<CommonModuleOptions>;
44+
}

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
export * from './logger/anonymizer';
1+
export * from './common.factory';
2+
export * from './common.module';
3+
export * from './common.options';
4+
25
export * from './logger/exception.handler';
36
export * from './logger/http-inspector-inbound.middleware';
47
export * from './logger/http-inspector-outbound.interceptor';
58
export * from './logger/logger.config';
9+
export * from './logger/obfuscator';
610

711
export * from './utils/compression.config';
812
export * from './utils/cors.config';

src/logger/exception.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class CustomExceptionHandler implements NestInterceptor {
6767
}
6868
}
6969

70-
export const configureExceptionLogger = () => (app: INestApplication) => {
70+
export const configureExceptionHandler = (app: INestApplication) => {
7171
const contextService = app.get(ContextService);
7272
app.useGlobalInterceptors(new CustomExceptionHandler(contextService));
7373
Logger.log('Exceptions handler initialized', '@gedai/common/config');

src/logger/http-inspector-inbound.middleware.ts

Lines changed: 28 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {
44
Logger,
55
NestMiddleware,
66
} from '@nestjs/common';
7-
import { ConfigService } from '@nestjs/config';
87
import { NextFunction, Request, Response } from 'express';
8+
import { MODULE_OPTIONS_TOKEN } from '../common.builder';
9+
import { CommonModuleOptions } from '../common.options';
910

1011
@Injectable()
1112
class HttpInspectorInboundMiddleware implements NestMiddleware {
@@ -82,57 +83,33 @@ class HttpInspectorInboundMiddleware implements NestMiddleware {
8283
}
8384
}
8485

85-
type InspectionOptions = {
86-
ignoreRoutes?: string[];
87-
};
86+
export const configureHttpInspectorInbound = (app: INestApplication) => {
87+
const options = app.get<CommonModuleOptions>(MODULE_OPTIONS_TOKEN);
88+
const { ignoreRoutes = [], mode = 'inbound' } =
89+
options.httpTrafficInspection ?? {};
90+
if (!['all', 'inbound'].includes(mode)) {
91+
return app;
92+
}
8893

89-
/**
90-
* Configures a globally bound middleware to inspect inbound http traffic.
91-
* @param {InspectionOptions} opts - configuration object specifying:
92-
*
93-
* - `ignoreRoutes` - a list of `request.path` routes to ignore
94-
*
95-
* ### Ignored Routes
96-
* #### Wildcards:
97-
* - \* matches N tokens in the `request.path`
98-
* #### Examples:
99-
* - '/v1/accounts/\*\/holder'
100-
* - - hides '/v1/accounts/:id/holder' from inspection
101-
* - '/v1/accounts/*'
102-
* - - Hides nested route inside '/v1/accounts' from inspection
103-
*
104-
*/
105-
export const configureHttpInspectorInbound =
106-
(opts?: InspectionOptions) => (app: INestApplication) => {
107-
const { ignoreRoutes = [] } = opts || {};
108-
const configService = app.get(ConfigService);
109-
const httpInspection = configService.get(
110-
'TRAFFIC_INSPECTION_HTTP',
111-
'inbound',
94+
if (ignoreRoutes) {
95+
Logger.log(
96+
{
97+
message: 'HTTP Inspection is set to ignore routes',
98+
routes: ignoreRoutes,
99+
},
100+
'@gedai/common/config',
112101
);
113-
if (!['all', 'inbound'].includes(httpInspection)) {
114-
return app;
115-
}
116-
117-
if (ignoreRoutes) {
118-
Logger.log(
119-
{
120-
message: 'HTTP Inspection is set to ignore routes',
121-
routes: ignoreRoutes,
122-
},
123-
'@gedai/common/config',
124-
);
125-
}
102+
}
126103

127-
const inspector = new HttpInspectorInboundMiddleware(
128-
ignoreRoutes.map((x) => new RegExp(`^${x.replace('*', '.+')}$`, 'i')),
129-
);
130-
const middleware = inspector.use.bind(inspector);
104+
const inspector = new HttpInspectorInboundMiddleware(
105+
ignoreRoutes.map((x) => new RegExp(`^${x.replace('*', '.+')}$`, 'i')),
106+
);
107+
const middleware = inspector.use.bind(inspector);
131108

132-
Object.defineProperty(middleware, 'name', {
133-
value: HttpInspectorInboundMiddleware.name,
134-
});
135-
app.use(middleware);
136-
Logger.log('Inbound http inspection initialized', '@gedai/common/config');
137-
return app;
138-
};
109+
Object.defineProperty(middleware, 'name', {
110+
value: HttpInspectorInboundMiddleware.name,
111+
});
112+
app.use(middleware);
113+
Logger.log('Inbound http inspection initialized', '@gedai/common/config');
114+
return app;
115+
};

src/logger/http-inspector-outbound.interceptor.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { INestApplication, Logger } from '@nestjs/common';
2-
import { ConfigService } from '@nestjs/config';
32
import * as http from 'http';
43
import * as https from 'https';
4+
import { MODULE_OPTIONS_TOKEN } from '../common.builder';
5+
import { CommonModuleOptions } from '../common.options';
56
import { logRequestError, logResponse } from './http-inspector.utils';
67

78
const handleResponse =
@@ -73,13 +74,13 @@ function mountInterceptor(logger: Logger, module: typeof http | typeof https) {
7374
}
7475
}
7576

76-
export const configureHttpInspectorOutbound = () => (app: INestApplication) => {
77-
const configService = app.get(ConfigService);
78-
const httpInspection = configService.get('TRAFFIC_INSPECTION_HTTP', 'none');
79-
if (!['all', 'outbound'].includes(httpInspection)) {
77+
export const configureHttpInspectorOutbound = (app: INestApplication) => {
78+
const options = app.get<CommonModuleOptions>(MODULE_OPTIONS_TOKEN);
79+
// TODO: add ignore routes
80+
const { mode } = options.httpTrafficInspection ?? {};
81+
if (!['all', 'outbound'].includes(mode)) {
8082
return app;
8183
}
82-
8384
const logger = new Logger('OutboundHTTPInspection');
8485
for (const module of [http, https]) {
8586
mountInterceptor(logger, module);

src/logger/logger.config.ts

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { Context, ContextService } from '@gedai/nestjs-core';
22
import { INestApplication, Logger } from '@nestjs/common';
3-
import { ConfigService } from '@nestjs/config';
43
import {
54
WinstonModule,
65
WinstonModuleOptions,
76
utilities as nestWinstonUtils,
87
} from 'nest-winston';
98
import { config, format, transports } from 'winston';
10-
import { Anonymizer, RegExpAnonymizer } from './anonymizer';
9+
import { MODULE_OPTIONS_TOKEN } from '../common.builder';
10+
import { CommonModuleOptions } from '../common.options';
11+
import { Obfuscator, RegExpObfuscator } from './obfuscator';
1112

1213
let contextService: ContextService;
13-
let anonymizer: Anonymizer;
14+
let anonymizer: Obfuscator;
1415
let env: string;
1516
let serviceName: string;
1617

@@ -37,11 +38,11 @@ let extraSensitiveKeys: (string | RegExp)[];
3738

3839
const sensitive = () =>
3940
format((info) => {
40-
const anonymized = anonymizer.maskFields(info, [
41+
const obfuscated = anonymizer.obfuscate(info, [
4142
...(extraSensitiveKeys ?? []),
4243
...commonSensitiveKeys,
4344
]);
44-
return anonymized;
45+
return obfuscated;
4546
})();
4647

4748
const environment = () =>
@@ -94,41 +95,42 @@ const prettyFormat = () =>
9495
nestLike(serviceName),
9596
);
9697

97-
export type LoggerOptions = {
98-
silent?: boolean;
99-
anonymizer?: Anonymizer;
100-
anonymizeKeys?: (string | RegExp)[];
101-
};
102-
103-
export const configureLogger =
104-
(options?: LoggerOptions) => (app: INestApplication) => {
105-
const {
106-
anonymizer: _anonymizer = new RegExpAnonymizer(),
107-
silent = false,
108-
anonymizeKeys,
109-
} = options || {};
110-
const configService = app.get(ConfigService);
111-
contextService = app.get(ContextService);
112-
extraSensitiveKeys = anonymizeKeys;
113-
anonymizer = _anonymizer;
114-
115-
const _env = configService.get('NODE_ENV', 'production');
116-
const appName = configService.get('SERVICE_NAME', 'nest-app');
117-
const logLevel = configService.get('LOG_LEVEL', 'info');
118-
const logFormat = configService.get('LOG_FORMAT', 'json');
119-
const usePrettyFormat = logFormat === 'pretty';
120-
121-
env = _env;
122-
serviceName = appName;
123-
const loggerConfig: WinstonModuleOptions = {
124-
silent,
125-
levels: config.npm.levels,
126-
level: logLevel,
127-
format: usePrettyFormat ? prettyFormat() : jsonFormat(),
128-
transports: [new Console()],
129-
};
130-
const logger = WinstonModule.createLogger(loggerConfig);
131-
app.useLogger(logger);
132-
Logger.log('Logger initialized', '@gedai/common/config');
133-
return app;
98+
export const configureLogger = (app: INestApplication) => {
99+
const options = app.get<CommonModuleOptions>(MODULE_OPTIONS_TOKEN);
100+
const {
101+
appName = 'unknown-app',
102+
environment = 'production',
103+
logger: loggerConfig = {},
104+
} = options;
105+
106+
const {
107+
format = 'json',
108+
level = 'info',
109+
silent = false,
110+
obfuscation = {},
111+
} = loggerConfig;
112+
113+
const {
114+
sensitiveKeys: anonymizeKeys = [],
115+
obfuscator: _anonymizer = new RegExpObfuscator(),
116+
} = obfuscation;
117+
118+
contextService = app.get(ContextService);
119+
extraSensitiveKeys = anonymizeKeys;
120+
anonymizer = _anonymizer;
121+
const usePrettyFormat = format === 'pretty';
122+
123+
env = environment;
124+
serviceName = appName;
125+
const winstonConfig: WinstonModuleOptions = {
126+
silent,
127+
levels: config.npm.levels,
128+
level,
129+
format: usePrettyFormat ? prettyFormat() : jsonFormat(),
130+
transports: [new Console()],
134131
};
132+
const logger = WinstonModule.createLogger(winstonConfig);
133+
app.useLogger(logger);
134+
Logger.log('Logger initialized', '@gedai/common/config');
135+
return app;
136+
};

0 commit comments

Comments
 (0)