Repository: RobinTail/express-zod-api
Pull request #96 (2021-08-12 16:20:31)

Fixing issue with a separate config file
In some cases there is a type mismatch issue when the configuration is declared in a separate file using the `ConfigType`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6984629..a487f5a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,37 @@
 
 ## Version 2
 
+### v2.3.1
+
+- Fixed a type mismatch issue when the configuration is declared in a separate file using the `ConfigType`.
+  - `ConfigType` is now deprecated *(will be removed in v3)*.
+  - Please use helper function `createConfig()`.
+  - This way it assigns the correct type for using configuration with `createServer()` and `attachRouting()`.
+```typescript
+// before
+const configBefore: ConfigType = {
+  server: {
+    listen: 8090,
+  },
+  cors: true,
+  logger: {
+    level: 'debug',
+    color: true
+  }
+};
+// after
+export const configAfter = createConfig({
+  server: {
+    listen: 8090,
+  },
+  cors: true,
+  logger: {
+    level: 'debug',
+    color: true
+  },
+});
+```
+
 ### v2.3.0
 
 - Changes and improvements of the generated Swagger / OpenAPI documentation:
diff --git a/README.md b/README.md
index 2872f97d..9124200a 100644
--- a/README.md
+++ b/README.md
@@ -94,9 +94,9 @@ See the [full implementation example here](https://github.com/RobinTail/express-
 ## Set up config
 
 ```typescript
-import {ConfigType} from 'express-zod-api';
+import {createConfig} from 'express-zod-api';
 
-const config: ConfigType = {
+const config = createConfig({
   server: {
     listen: 8090,
   },
@@ -105,7 +105,7 @@ const config: ConfigType = {
     level: 'debug',
     color: true
   }
-};
+});
 ```
 *See all available options [here](https://github.com/RobinTail/express-zod-api/blob/master/src/config-type.ts).*
 
@@ -338,12 +338,12 @@ You can specify your custom Winston logger in config:
 
 ```typescript
 import * as winston from 'winston';
-import {ConfigType, createServer} from 'express-zod-api';
+import {createConfig, createServer} from 'express-zod-api';
 
-const config: ConfigType = {
+const config = createConfig({
   logger: winston.createLogger(),
   ...
-};
+});
 createServer(config, routing);
 ```
 
@@ -353,10 +353,10 @@ You can instantiate your own express app and connect your endpoints the followin
 
 ```typescript
 import * as express from 'express';
-import {ConfigType, attachRouting} from 'express-zod-api';
+import {createConfig, attachRouting} from 'express-zod-api';
 
 const app = express();
-const config: ConfigType = {app, ...};
+const config = createConfig({app, ...});
 const routing = {...};
 
 attachRouting(config, routing);
diff --git a/coverage.svg b/coverage.svg
index e913cdf8..9ef14ec7 100644
--- a/coverage.svg
+++ b/coverage.svg
@@ -1,1 +1,1 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="110" height="20" role="img" aria-label="Coverage: 98.4%"><title>Coverage: 98.4%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="110" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="47" height="20" fill="#4c1"/><rect width="110" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="855" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">98.4%</text><text x="855" y="140" transform="scale(.1)" fill="#fff" textLength="370">98.4%</text></g></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 98.42%"><title>Coverage: 98.42%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">98.42%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">98.42%</text></g></svg>
\ No newline at end of file
diff --git a/example/index.ts b/example/index.ts
index aa65bb37..7131bf5e 100644
--- a/example/index.ts
+++ b/example/index.ts
@@ -1,8 +1,7 @@
-import {ConfigType} from '../src';
-import {createServer} from '../src';
+import {createConfig, createServer} from '../src';
 import {routing} from './routing';
 
-const config: ConfigType = {
+const config = createConfig({
   server: {
     listen: 8090,
   },
@@ -11,6 +10,6 @@ const config: ConfigType = {
     level: 'debug',
     color: true
   }
-};
+});
 
 createServer(config, routing);
diff --git a/src/config-type.ts b/src/config-type.ts
index 4443e579..4a794e26 100644
--- a/src/config-type.ts
+++ b/src/config-type.ts
@@ -14,16 +14,18 @@ export interface LoggerConfig {
   color: boolean;
 }
 
-export type ConfigType = ({
+export interface ServerConfig {
   server: { // server configuration
-    // port or socket
-    listen: number | string;
-    // custom JSON parser, default: express.json()
-    jsonParser?: NextHandleFunction;
+    listen: number | string; // port or socket
+    jsonParser?: NextHandleFunction; // custom JSON parser, default: express.json()
   },
-} | { // or your custom express app
-  app: Express
-}) & {
+}
+
+export interface AppConfig {
+  app: Express; // or your custom express app
+}
+
+export interface CommonConfig {
   // enable cross-origin resource sharing
   // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
   cors: boolean;
@@ -33,3 +35,13 @@ export type ConfigType = ({
   // logger configuration or your custom winston logger
   logger: LoggerConfig | Logger;
 }
+
+export const createConfig = <T extends (ServerConfig | AppConfig) & CommonConfig>(config: T): T => config;
+
+/**
+ * @since v2.3.1
+ * @deprecated
+ * @see createConfig()
+ * @todo remove in v3
+ * */
+export type ConfigType = (ServerConfig | AppConfig) & CommonConfig;
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 41f5f1ea..ead3cb6a 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -1,7 +1,7 @@
 import {Request, Response} from 'express';
 import {Logger} from 'winston';
 import {z} from 'zod';
-import {ConfigType} from './config-type';
+import {CommonConfig} from './config-type';
 import {
   ApiResponse,
   combineEndpointAndMiddlewareInputSchemas,
@@ -28,7 +28,7 @@ export abstract class AbstractEndpoint {
     request: Request,
     response: Response,
     logger: Logger,
-    config: ConfigType
+    config: CommonConfig
   }): Promise<void>;
 
   public getDescription() {
@@ -210,7 +210,7 @@ export class Endpoint<
     request: Request,
     response: Response,
     logger: Logger,
-    config: ConfigType
+    config: CommonConfig
   }) {
     let output: any;
     let error: Error | null = null;
diff --git a/src/index.ts b/src/index.ts
index 5d56f72a..98551248 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-export { ConfigType, LoggerConfig } from './config-type';
+export { createConfig, LoggerConfig, ConfigType } from './config-type';
 export { AbstractEndpoint, EndpointInput, EndpointOutput, EndpointResponse } from './endpoint';
 export { Method } from './method';
 export { EndpointsFactory, defaultEndpointsFactory } from './endpoints-factory';
diff --git a/src/routing.ts b/src/routing.ts
index 223a2e70..604e24f2 100644
--- a/src/routing.ts
+++ b/src/routing.ts
@@ -1,6 +1,6 @@
 import {Express} from 'express';
 import {Logger} from 'winston';
-import {ConfigType} from './config-type';
+import {CommonConfig} from './config-type';
 import {AbstractEndpoint, Endpoint} from './endpoint';
 import {DependsOnMethodError, RoutingError} from './errors';
 import {AuxMethod, Method} from './method';
@@ -86,7 +86,7 @@ export const routingCycle = ({routing, cb, parentPath, cors}: RoutingCycleParams
 export const initRouting = ({app, logger, config, routing}: {
   app: Express,
   logger: Logger,
-  config: ConfigType,
+  config: CommonConfig,
   routing: Routing
 }) => {
   routingCycle({
diff --git a/src/server.ts b/src/server.ts
index 9cac238f..20d8e778 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,21 +1,18 @@
 import express from 'express';
 import {Server} from 'http';
-import {ConfigType} from './config-type';
+import {AppConfig, CommonConfig, ServerConfig} from './config-type';
 import {isLoggerConfig} from './helpers';
 import {createLogger} from './logger';
 import {defaultResultHandler} from './result-handler';
 import {initRouting, Routing} from './routing';
 import createHttpError from 'http-errors';
 
-type ConfigWithServer = Exclude<ConfigType, {app: any}>;
-type ConfigWithApp = Exclude<ConfigType, {server: any}>;
-
-export function attachRouting(config: ConfigWithApp, routing: Routing): void {
+export function attachRouting(config: AppConfig & CommonConfig, routing: Routing): void {
   const logger = isLoggerConfig(config.logger) ? createLogger(config.logger) : config.logger;
   return initRouting({app: config.app, routing, logger, config});
 }
 
-export function createServer(config: ConfigWithServer, routing: Routing): Server {
+export function createServer(config: ServerConfig & CommonConfig, routing: Routing): Server {
   const logger = isLoggerConfig(config.logger) ? createLogger(config.logger) : config.logger;
   const app = express();
   const errorHandler = config.errorHandler || defaultResultHandler;


Test patch:
diff --git a/tests/unit/config-type.spec.ts b/tests/unit/config-type.spec.ts
new file mode 100644
index 00000000..3d2d029c
--- /dev/null
+++ b/tests/unit/config-type.spec.ts
@@ -0,0 +1,34 @@
+import {Express} from 'express';
+import {createConfig} from '../../src';
+
+describe('ConfigType', () => {
+  describe('createConfig()', () => {
+    test('should create a config with server', () => {
+      const argument = {
+        server: {
+          listen: 3333,
+        },
+        cors: true,
+        logger: {
+          level: 'debug' as const,
+          color: false,
+        }
+      };
+      const config = createConfig(argument);
+      expect(config).toEqual(argument);
+    });
+
+    test('should create a config with app', () => {
+      const argument = {
+        app: jest.fn() as unknown as Express,
+        cors: true,
+        logger: {
+          level: 'debug' as const,
+          color: false,
+        }
+      };
+      const config = createConfig(argument);
+      expect(config).toEqual(argument);
+    });
+  });
+});
diff --git a/tests/unit/endpoint.spec.ts b/tests/unit/endpoint.spec.ts
index 32128543..a1791c25 100644
--- a/tests/unit/endpoint.spec.ts
+++ b/tests/unit/endpoint.spec.ts
@@ -2,13 +2,13 @@ import {expectType} from 'tsd';
 import {
   z,
   EndpointsFactory,
-  ConfigType,
   createMiddleware,
   defaultResultHandler,
   EndpointInput,
   EndpointOutput,
   EndpointResponse
 } from '../../src';
+import {CommonConfig} from '../../src/config-type';
 import {Endpoint} from '../../src/endpoint';
 import {Request, Response} from 'express';
 
@@ -100,14 +100,12 @@ describe('Endpoint', () => {
         json: jest.fn().mockImplementation(() => responseMock)
       };
       const configMock = {
-        server: {
-          cors: true
-        }
+        cors: true
       };
       await endpoint.execute({
         request: requestMock as Request,
         response: responseMock as any as Response,
-        config: configMock as unknown as ConfigType,
+        config: configMock as CommonConfig,
         logger: loggerMock
       });
       expect(middlewareMock).toBeCalledTimes(1);
diff --git a/tests/unit/routing.spec.ts b/tests/unit/routing.spec.ts
index 4ebf17dd..39a1089a 100644
--- a/tests/unit/routing.spec.ts
+++ b/tests/unit/routing.spec.ts
@@ -1,6 +1,7 @@
 import {Express, RequestHandler, Request, Response} from 'express';
 import {Logger} from 'winston';
-import {EndpointsFactory, z, Routing, ConfigType, DependsOnMethod, defaultResultHandler} from '../../src';
+import {EndpointsFactory, z, Routing, DependsOnMethod, defaultResultHandler} from '../../src';
+import {CommonConfig} from '../../src/config-type';
 import {initRouting} from '../../src/routing';
 
 let appMock: any;
@@ -62,7 +63,7 @@ describe('Routing', () => {
       initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: routing
       });
       expect(appMock.get).toBeCalledTimes(2);
@@ -117,7 +118,7 @@ describe('Routing', () => {
       initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: routing
       });
       expect(appMock.get).toBeCalledTimes(1);
@@ -153,7 +154,7 @@ describe('Routing', () => {
       initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: routing
       });
       expect(appMock.get).toBeCalledTimes(1);
@@ -183,7 +184,7 @@ describe('Routing', () => {
       initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: routing
       });
       expect(appMock.get).toBeCalledTimes(2);
@@ -204,7 +205,7 @@ describe('Routing', () => {
       expect(() => initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: {
           v1: {
             'user/retrieve': endpointMock
@@ -214,7 +215,7 @@ describe('Routing', () => {
       expect(() => initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: {
           'v1/user/retrieve': endpointMock
         }
@@ -245,7 +246,7 @@ describe('Routing', () => {
       initRouting({
         app: appMock as Express,
         logger: loggerMock as Logger,
-        config: configMock as ConfigType,
+        config: configMock as CommonConfig,
         routing: routing
       });
       expect(appMock.post).toBeCalledTimes(1);
diff --git a/tests/unit/server.spec.ts b/tests/unit/server.spec.ts
index 1de82880..aced0dd0 100644
--- a/tests/unit/server.spec.ts
+++ b/tests/unit/server.spec.ts
@@ -18,7 +18,8 @@ const expressMock = jest.mock('express', () => {
 });
 
 import express from 'express'; // mocked above
-import {ConfigType, createServer, attachRouting, EndpointsFactory, z, defaultResultHandler} from '../../src';
+import {createServer, attachRouting, EndpointsFactory, z, defaultResultHandler} from '../../src';
+import {AppConfig, CommonConfig, ServerConfig} from '../../src/config-type';
 
 describe('Server', () => {
   beforeEach(() => {
@@ -35,7 +36,7 @@ describe('Server', () => {
 
   describe('createServer()', () => {
     test('Should create server with minimal config', () => {
-      const configMock: ConfigType = {
+      const configMock: ServerConfig & CommonConfig = {
         server: {
           listen: 8054,
         },
@@ -104,7 +105,7 @@ describe('Server', () => {
             })
         }
       };
-      const server = createServer(configMock as unknown as ConfigType & {server: any}, routingMock);
+      const server = createServer(configMock as unknown as ServerConfig & CommonConfig, routingMock);
       expect(server).toBeInstanceOf(http.Server);
       expect(appMock).toBeTruthy();
       expect(appMock.use).toBeCalledTimes(2);
@@ -157,7 +158,7 @@ describe('Server', () => {
         }
       };
       // noinspection JSVoidFunctionReturnValueUsed
-      const result = attachRouting(configMock as unknown as ConfigType & {app: any}, routingMock);
+      const result = attachRouting(configMock as unknown as AppConfig & CommonConfig, routingMock);
       expect(result).toBe(undefined);
       expect(appMock.use).toBeCalledTimes(0);
       expect(configMock.errorHandler.handler).toBeCalledTimes(0);