sqmicro-http-service
Version:
HTTP microservice framework 4 SQ Analytics.
232 lines (203 loc) • 6.38 kB
JavaScript
// 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');
}
};