UNPKG

sqmicro-http-service

Version:

HTTP microservice framework 4 SQ Analytics.

232 lines (203 loc) 6.38 kB
// Config files may be read from `${process.cwd()}/config`. // See [Submodule configuration]{@link https://github.com/lorenwest/node-config/wiki/Sub-Module-Configuration}. process.env.SUPPRESS_NO_CONFIG_WARNING = 'y'; const config = require('config'); const express = require('express'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const multipartFormParser = require('connect-busboy'); const cors = require('cors'); let log; try { const commons = require('sqmicro-commons'); log = commons.log; } catch(e) { // nop } const getLog = function() { if (!log) { throw Error('Logger is not available: either override the `log` getter or install sqmicro-commons'); } return log; }; /** * Default cluster config. * Listen all interfaces on port 40001. Default middleware config. */ const DEFAULTS = { host: '0.0.0.0', port: 40001, bodyParser: { json: { }, urlencoded: { }, multipart: { } }, cookieParser: { }, cors: { } }; /** * Basic HTTP API service, that includes logger, set of body parsers, cors and * cookie parsers. Every piece of the functionality may be overriden in daughter * classes. */ module.exports = class Service { /** * Get logger. Override if required. */ static get log() { return getLog(); } static get processEventHandlers() { return { unhandledRejection: (reason, promise) => this.onUnhandledRejection(reason, promise), uncaughtException: (error) => this.onUncaughtException(error) }; } static bindProcessEvents() { Object.entries(this.processEventHandlers).forEach( ([eventName, handler]) => process.on(eventName, handler) ); } static onUncaughtException(error) { this.log.error('Unhandled exception', error); process.exit(1); } // Generally reject handlers should be assigned to a promise within the same // loop turn where the promise itself was created, hence warn. static onUnhandledRejection(reason, promise) { this.log.warn( 'A promise that still has no rejection handler is rejected with the following reason:', reason ); } /** * The list of body parsers: body-parser.json, body-parser.urlencoded, * connect-busyboy. Override if required. * Config: cluster.bodyParser = {json, urlencoded, multipart} */ get bodyParsers() { return [].concat([ bodyParser.json(config.get('cluster.bodyParser.json')), bodyParser.urlencoded(config.get('cluster.bodyParser.urlencoded')), multipartFormParser(config.get('cluster.bodyParser.multipart')) ]); } /** * Cookie parser. Override if required. * Config: cluster.cookieParser. */ get cookieParser() { return cookieParser(config.get('cluster.cookieParser')); } /** * CORS handler using cors module. Override if required. * Config: cluster.cors. */ get corsHandler() { return cors(config.get('cluster.cors')); } /** * Get the list of error handlers that are appended to the bottom of the * middleware stack. Override to customize stacks. */ get errorHandlers() { return [ (err, req, res, next) => this.handleError(err, req, res) ]; } /** * Request error logger. Defines `logError(err)` instance method on Request. * May be overriden in a daughter class but take care of properly logging * unhandled errors then (see handleErrors(), errorHandlers)! * @example * (err, req, res, next) => { * req.logError(err); * next(err); * } */ get errorLogger() { const log = this.log; return function errorLogger(req, res, next) { req.logError = function logError(err) { log.error( 'Unhandled router exception. URL: %s; Headers: %j.', this.originalUrl, this.headers, err ); }; next(); }; } /** * Get logger. Override static getter instead of this. */ get log() { return this.constructor.log; } /** * Middleware, the ordered list: bodyParser, cookieParser, corsHandler, error logger. * Override if required. */ get middleware() { return [].concat( this.bodyParsers, this.cookieParser, this.corsHandler, this.errorLogger ); } // Either Config or plain object. constructor(options) { config.util.setModuleDefaults( 'cluster', config.util.extendDeep(DEFAULTS, options) ); this.checkConfig(config.get('cluster')); } /** * Starts the service, using the router. * @param {express.Router} router */ async start(router) { this.log.trace('Starting http service'); await this.startHttp(router); } async startHttp(router) { let service = express(); service.use(this.middleware); service.use(router); service.use(this.errorHandlers); let host = config.get('cluster.host'); let port = config.get('cluster.port'); await new Promise(fulfill => service.listen(port, host, fulfill)); this.log.info(`API service is listening at ${host}:${port}`); } /** * Check configuration. */ checkConfig(conf) { if (!conf.port) { throw Error('Specify port number'); } if (!conf.emptyImageUrl) { throw Error('Empty image url is missing'); } } /** * Log the error and respond with 500. Override if required. Attached after * all middleware and router. * @param {*} err The error. * @param {*} req The request. * @param {*} res The response. */ handleError(err, req, res) { // logError may not exist in daughter classes. if (req.logError) { req.logError(err); } this.log.trace('Responding with status code 500 due to an unhandled error'); res.status(500).send('Something went wrong.\n'); } };