Passed
Push — main ( d13a4d...a77d33 )
by Lorenzo
01:16 queued 14s
created

ExpressBeans.serializeRequest   C

Complexity

Conditions 10

Size

Total Lines 16
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 10

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 16
ccs 7
cts 7
cp 1
rs 5.9999
c 0
b 0
f 0
cc 10
crap 10

How to fix   Complexity   

Complexity

Complex classes like ExpressBeans.serializeRequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 5
import express, { Express, Request, Router } from 'express';
2 5
import { pinoHttp, startTime } from 'pino-http';
3
import { ServerResponse, IncomingMessage } from 'http';
4 5
import EventEmitter from 'events';
5
import { ExpressBeansOptions, ExpressRouterBean } from '@/ExpressBeansTypes';
6 5
import { logger } from '@/core';
7 5
import { Executor } from '@/core/Executor';
8
9
type ExpressBeanEventMap = {
10
  error: [Error];
11
  initialized: [];
12
}
13
14 5
export default class ExpressBeans extends EventEmitter<ExpressBeanEventMap> {
15 18
  private readonly app: Express;
16
17 18
  private readonly router: Router;
18
19
  /**
20
   * Creates a new ExpressBeans application
21
   * @param options {ExpressBeansOptions}
22
   */
23
  static async createApp(options?: Partial<ExpressBeansOptions>): Promise<ExpressBeans> {
24 5
    const app = new ExpressBeans({ ...options });
25 5
    return app;
26
  }
27
28
  /**
29
   * Constructor of ExpressBeans application
30
   * @constructor
31
   * @param options {ExpressBeansOptions}
32
   */
33
  constructor(options?: Partial<ExpressBeansOptions>) {
34 18
    super();
35 18
    this.router = express.Router();
36 18
    this.app = express();
37 18
    this.app.disable('x-powered-by');
38 18
    this.app.use(options?.baseURL ?? '/', this.router);
39 18
    if (options?.logRequests === undefined || options.logRequests) {
40 16
      this.router.use(pinoHttp(
41
        {
42
          logger,
43
          customSuccessMessage: this.serializeRequest.bind(this),
44
          customErrorMessage: this.serializeRequest.bind(this),
45
        },
46
      ));
47
    }
48 18
    Executor.setExecution('init', async () => this.initialize(options ?? {}));
49 18
    Executor.eventEmitter.on('error', () => {
50 5
      process.exit(1);
51
    });
52 18
    Executor.startLifecycle();
53
  }
54
55
  private serializeRequest(req: IncomingMessage, res: ServerResponse) {
56 11
    const request: Request = req as Request;
57 11
    const remoteAddress = request.headers['x-forwarded-for'] ?? request.socket.remoteAddress;
58 11
    const { method, originalUrl, httpVersion } = request;
59 11
    const responseTime = Date.now() - res[startTime];
60 11
    const optionals = [
61
      res.statusCode,
62
      res.getHeader('content-length'),
63
      res.getHeader('content-type'),
64
      request.headers.referer,
65
      request.headers['user-agent'],
66
    ]
67 55
      .filter((i) => !!i)
68
      .join(' ');
69 11
    return `${remoteAddress} - "${method} ${originalUrl} HTTP/${httpVersion}" ${optionals} - ${responseTime}ms`;
70
  }
71
72
  /**
73
   * Initializes the application and checks
74
   * if all beans are valid
75
   * @param listen {boolean}
76
   * @param port {number}
77
   * @param beans {Object[]}
78
   * @param onInitialized {Function}
79
   * @private
80
   */
81 15
  private async initialize({
82
    listen = true,
83
    port = 8080,
84
    routerBeans = [],
85
  }: Partial<ExpressBeansOptions>) {
86 15
    return Promise.resolve(routerBeans as Array<ExpressRouterBean>)
87
      .then(this.checkRouterBeans.bind(this))
88
      .then(this.registerRouters.bind(this))
89
      .then(() => {
90 13
        if (listen) {
91 5
          return new Promise<void>(
92
            (resolve, reject) => {
93 5
              this.listen(port, (err) => (err ? reject(err) : resolve()));
94
            },
95 3
          ).catch((err) => Promise.reject(err));
96
        }
97 8
        return Promise.resolve();
98
      });
99
  }
100
101
  /**
102
   * Starts the server and emits the initialized event
103
   * @param {number} port
104
   */
105
  listen(port: number, callback?: (error?: Error) => void) {
106 12
    return this.app.listen(port, (error) => {
107 12
      callback?.(error);
108 12
      if (error) {
109 3
        this.emit('error', error);
110
        return;
111
      }
112 9
      logger.info(`Server listening on port ${port}`);
113 9
      this.emit('initialized');
114
    });
115
  }
116
117
  /**
118
   * Registers all routers
119
   * @param routers {Array<ExpressRouterBean>}
120
   * @private
121
   */
122
  private async registerRouters(routers: Array<ExpressRouterBean>) {
123 14
    return new Promise((resolve, reject) => {
124 14
      Array.from(routers)
125 13
        .map((bean) => bean._instance)
126
        .forEach((instance) => {
127 13
          try {
128
            const {
129
              path,
130
              router,
131 13
            } = instance._routerConfig;
132 13
            logger.debug(`Registering router ${instance._className}`);
133 13
            this.router.use(path, router);
134
          } catch (e) {
135 1
            logger.error(e);
136 1
            reject(new Error(`Router ${instance._className} not initialized correctly`, { cause: e }));
137
          }
138
        });
139 14
      resolve(true);
140
    });
141
  }
142
143
  /**
144
   * Checks if all beans are valid
145
   * @param routerBeans {Array<ExpressRouterBean>}
146
   * @returns {Array<ExpressRouterBean>}
147
   * @throws {Error}
148
   * @private
149
   */
150
  private async checkRouterBeans(routerBeans: Array<ExpressRouterBean>):
151
  Promise<ExpressRouterBean[]> {
152 15
    const invalidBeans = routerBeans
153 16
      .filter(((bean) => !bean._beanUUID))
154 1
      .map((object: any) => object.prototype.constructor.name);
155 15
    if (invalidBeans.length > 0) {
156 1
      return Promise.reject(new Error(`Trying to use something that is not an ExpressBean: ${invalidBeans.join(', ')}`));
157
    }
158 14
    return routerBeans;
159
  }
160
161
  /**
162
   * Gets Express application
163
   * @returns {Express}
164
   */
165
  getApp() {
166 2
    return this.app;
167
  }
168
169
  /**
170
   * Exposes use function of Express application
171
   * @param handlers
172
   */
173
  use(...handlers: any) {
174 1
    this.app.use(...handlers);
175
  }
176
}
177