Repository: RobinTail/express-zod-api
Pull request #6 (2021-04-20 10:56:58)

Custom logger


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80ff77eb..70de7625 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## v0.4.0
+- Ability to specify your custom Winston logger in config
+- `createLogger()` now accepts `LoggerConfig` as an argument
+
+```typescript
+// before
+createLogger(config);
+// after
+createLogger(config.logger);
+```
+
 ## v0.3.1
 - Minor Readme file fixes and clarifications
 
diff --git a/README.md b/README.md
index 7a526dd5..2e6b418b 100644
--- a/README.md
+++ b/README.md
@@ -180,6 +180,21 @@ const authMiddleware = createMiddleware({
 })
 ```
 
+## Your custom logger
+
+You can specify your custom Winston logger in config:
+
+```typescript
+import * as winston from 'winston';
+import {ConfigType, createServer} from 'express-zod-api';
+
+const config: ConfigType = {
+   logger: winston.createLogger(),
+   ...
+};
+createServer(config, routing);
+```
+
 ## Your custom server
 
 You can instantiate your own express app and connect your endpoints the following way:
@@ -188,7 +203,7 @@ You can instantiate your own express app and connect your endpoints the followin
 import {ConfigType, createLogger, initRouting} from 'express-zod-api';
 
 const config: ConfigType = {...};
-const logger = createLogger(config);
+const logger = createLogger(config.logger);
 const routing = {...};
 
 initRouting({app, logger, config, routing});
diff --git a/src/config-type.ts b/src/config-type.ts
index 3b07ad64..c995088c 100644
--- a/src/config-type.ts
+++ b/src/config-type.ts
@@ -1,6 +1,18 @@
 import {NextHandleFunction} from 'connect';
+import * as winston from 'winston';
 import {ResultHandler} from './result-handler';
 
+export const loggerLevels = {
+  silent: true,
+  warn: true,
+  debug: true,
+};
+
+export interface LoggerConfig {
+  level: keyof typeof loggerLevels;
+  color: boolean;
+}
+
 export interface ConfigType {
   server: {
     // port or socket
@@ -13,8 +25,6 @@ export interface ConfigType {
     // custom handler for errors and output, default: defaultResultHandler()
     resultHandler?: ResultHandler;
   },
-  logger: {
-    level: 'silent' | 'warn' | 'debug';
-    color: boolean;
-  }
+  // logger configuration or your custom winston logger
+  logger: LoggerConfig | winston.Logger;
 }
diff --git a/src/helpers.ts b/src/helpers.ts
index 4a6b57b1..79fb7b7b 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,5 +1,6 @@
 import {Request} from 'express';
 import {z} from 'zod';
+import {LoggerConfig, loggerLevels} from './config-type';
 import {MiddlewareDefinition} from './middleware';
 
 export type FlatObject = Record<string, any>;
@@ -38,3 +39,10 @@ export function getInitialInput(request: Request): any {
       return {...request.query, ...request.body};
   }
 }
+
+export function isLoggerConfig(logger: any): logger is LoggerConfig {
+  return typeof logger === 'object' &&
+    'level' in logger && 'color' in logger &&
+    Object.keys(loggerLevels).includes(logger.level) &&
+    typeof logger.color === 'boolean';
+}
diff --git a/src/index.ts b/src/index.ts
index 63a0c3b4..297b4e9b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-export { ConfigType } from './config-type';
+export { ConfigType, LoggerConfig } from './config-type';
 export { AbstractEndpoint, Method, EndpointInput, EndpointOutput } from './endpoint';
 export { EndpointsFactory } from './endpoints-factory';
 export { ObjectSchema, FlatObject } from './helpers';
diff --git a/src/logger.ts b/src/logger.ts
index 6af73af8..c6809e95 100644
--- a/src/logger.ts
+++ b/src/logger.ts
@@ -3,16 +3,16 @@ import {Format} from 'logform';
 import {LEVEL, MESSAGE, SPLAT} from 'triple-beam';
 import * as winston from 'winston';
 import * as Transport from 'winston-transport';
-import {ConfigType} from './config-type';
+import {LoggerConfig} from './config-type';
 
 const {combine, colorize, timestamp: useTimestamp, printf} = winston.format;
 
-export function createLogger(config: ConfigType): winston.Logger {
+export function createLogger(loggerConfig: LoggerConfig): winston.Logger {
   const prettyPrint = (meta: any) => {
     delete meta[LEVEL];
     delete meta[MESSAGE];
     delete meta[SPLAT];
-    return inspect(meta, false, 1, config.logger.color);
+    return inspect(meta, false, 1, loggerConfig.color);
   };
 
   const getOutputFormat = (isPretty?: boolean) => printf(
@@ -30,11 +30,11 @@ export function createLogger(config: ConfigType): winston.Logger {
     handleExceptions: true,
   };
 
-  if (config.logger.color) {
+  if (loggerConfig.color) {
     formats.push(colorize());
   }
 
-  switch (config.logger.level) {
+  switch (loggerConfig.level) {
     case 'debug':
       consoleOutputOptions.level = 'debug';
       formats.push(getOutputFormat(true));
@@ -49,7 +49,7 @@ export function createLogger(config: ConfigType): winston.Logger {
   consoleOutputOptions.format = combine(...formats);
 
   return winston.createLogger({
-    silent: config.logger.level === 'silent',
+    silent: loggerConfig.level === 'silent',
     levels: winston.config.npm.levels,
     transports: [new winston.transports.Console(consoleOutputOptions)],
     exitOnError: false,
diff --git a/src/server.ts b/src/server.ts
index ef00ec5a..869496d2 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,12 +1,13 @@
 import * as express from 'express';
 import {ConfigType} from './config-type';
+import {isLoggerConfig} from './helpers';
 import {createLogger} from './logger';
 import {defaultResultHandler} from './result-handler';
 import {initRouting, Routing} from './routing';
 import * as createHttpError from 'http-errors';
 
 export function createServer(config: ConfigType, routing: Routing) {
-  const logger = createLogger(config);
+  const logger = isLoggerConfig(config.logger) ? createLogger(config.logger) : config.logger;
   const app = express();
   const resultHandler = config.server.resultHandler || defaultResultHandler;
 


Test patch:
diff --git a/tests/unit/helpers.spec.ts b/tests/unit/helpers.spec.ts
index 330d3398..b4231f0d 100644
--- a/tests/unit/helpers.spec.ts
+++ b/tests/unit/helpers.spec.ts
@@ -1,4 +1,4 @@
-import {combineEndpointAndMiddlewareInputSchemas, getInitialInput} from '../../src/helpers';
+import {combineEndpointAndMiddlewareInputSchemas, getInitialInput, isLoggerConfig} from '../../src/helpers';
 import {createMiddleware, z} from '../../src';
 import {Request} from 'express';
 
@@ -33,6 +33,7 @@ describe('Helpers', () => {
       expect(result.shape).toMatchSnapshot();
     });
   });
+  
   describe('getInitialInput()', () => {
     test('should return body for POST, PUT and PATCH requests', () => {
       expect(getInitialInput({
@@ -65,4 +66,35 @@ describe('Helpers', () => {
       });
     });
   });
+
+  describe('isLoggerConfig()', () => {
+    test('Should identify the valid logger config', () => {
+      expect(isLoggerConfig({
+        level: 'debug',
+        color: true,
+      })).toBeTruthy();
+    });
+    test('Should reject the object with invalid properties', () => {
+      expect(isLoggerConfig({
+        level: 'something',
+        color: true,
+      })).toBeFalsy();
+      expect(isLoggerConfig({
+        level: 'debug',
+        color: null,
+      })).toBeFalsy();
+    });
+    test('Should reject the object with missing properties', () => {
+      expect(isLoggerConfig({
+        level: 'something',
+      })).toBeFalsy();
+      expect(isLoggerConfig({
+        color: null,
+      })).toBeFalsy();
+    });
+    test('Should reject non-objects', () => {
+      expect(isLoggerConfig([1,2,3])).toBeFalsy();
+      expect(isLoggerConfig('something')).toBeFalsy();
+    });
+  });
 });
diff --git a/tests/unit/logger.spec.ts b/tests/unit/logger.spec.ts
index dafec998..176eda5a 100644
--- a/tests/unit/logger.spec.ts
+++ b/tests/unit/logger.spec.ts
@@ -1,15 +1,13 @@
-import {ConfigType, createLogger} from '../../src';
+import {createLogger, LoggerConfig} from '../../src';
 
 describe('Logger', () => {
   describe('createLogger()', () => {
     test('Should create silent logger', () => {
-      const configMock = {
-        logger: {
-          level: 'silent',
-          color: false
-        }
+      const loggerConfig: LoggerConfig = {
+        level: 'silent',
+        color: false
       };
-      const logger = createLogger(configMock as ConfigType);
+      const logger = createLogger(loggerConfig);
       expect(logger.silent).toBeTruthy();
       expect(logger.isErrorEnabled()).toBeTruthy();
       expect(logger.isWarnEnabled()).toBeTruthy();
@@ -20,13 +18,11 @@ describe('Logger', () => {
     });
 
     test('Should create warn logger', () => {
-      const configMock = {
-        logger: {
-          level: 'warn',
-          color: false
-        }
+      const loggerConfig: LoggerConfig = {
+        level: 'warn',
+        color: false
       };
-      const logger = createLogger(configMock as ConfigType);
+      const logger = createLogger(loggerConfig);
       expect(logger.isErrorEnabled()).toBeTruthy();
       expect(logger.isWarnEnabled()).toBeTruthy();
       expect(logger.isInfoEnabled()).toBeFalsy();
@@ -36,13 +32,11 @@ describe('Logger', () => {
     });
 
     test('Should create debug logger', () => {
-      const configMock = {
-        logger: {
-          level: 'debug',
-          color: false
-        }
+      const loggerConfig: LoggerConfig = {
+        level: 'debug',
+        color: false
       };
-      const logger = createLogger(configMock as ConfigType);
+      const logger = createLogger(loggerConfig);
       expect(logger.isErrorEnabled()).toBeTruthy();
       expect(logger.isWarnEnabled()).toBeTruthy();
       expect(logger.isInfoEnabled()).toBeTruthy();
diff --git a/tests/unit/server.spec.ts b/tests/unit/server.spec.ts
index 7ee320a0..49f7b072 100644
--- a/tests/unit/server.spec.ts
+++ b/tests/unit/server.spec.ts
@@ -2,7 +2,7 @@ let appMock: ReturnType<typeof newAppMock>;
 const expressJsonMock = jest.fn();
 const newAppMock = () => ({
   use: jest.fn(),
-  listen: jest.fn(),
+  listen: jest.fn((port, cb) => cb()),
   get: jest.fn(),
   post: jest.fn()
 });
@@ -68,8 +68,8 @@ describe('Server', () => {
       expect(appMock.listen.mock.calls[0][0]).toBe(8054);
     });
 
-    test('Should create server with custom JSON parser and result handler', () => {
-      const configMock: ConfigType = {
+    test('Should create server with custom JSON parser, logger and result handler', () => {
+      const configMock = {
         server: {
           listen: 8054,
           cors: true,
@@ -77,8 +77,7 @@ describe('Server', () => {
           resultHandler: jest.fn()
         },
         logger: {
-          level: 'warn',
-          color: false
+          info: jest.fn()
         }
       };
       const routingMock = {
@@ -95,7 +94,7 @@ describe('Server', () => {
           })
         }
       };
-      createServer(configMock, routingMock);
+      createServer(configMock as unknown as ConfigType, routingMock);
       expect(appMock).toBeTruthy();
       expect(appMock.use).toBeCalledTimes(2);
       expect(Array.isArray(appMock.use.mock.calls[0][0])).toBeTruthy();
@@ -104,6 +103,8 @@ describe('Server', () => {
       expect(configMock.server.resultHandler).toBeCalledTimes(0);
       appMock.use.mock.calls[1][0]({method: 'get', path: '/v1/test'});
       expect(configMock.server.resultHandler).toBeCalledTimes(1);
+      expect(configMock.logger.info).toBeCalledTimes(1);
+      expect(configMock.logger.info).toBeCalledWith('Listening 8054');
       expect(appMock.get).toBeCalledTimes(1);
       expect(appMock.get.mock.calls[0][0]).toBe('/v1/test');
       expect(appMock.post).toBeCalledTimes(1);