expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
455 lines (454 loc) • 17.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = void 0;
const utils_1 = require("./utils");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const express = require("express");
const fs = require("fs");
const http = require("http");
const https = require("https");
const http2 = require("http2");
const jaul = require("jaul");
const logger = require("anyhow");
const path = require("path");
const setmeup = require("setmeup");
let settings;
class App {
static _instance;
static get Instance() {
return this._instance || (this._instance = new this());
}
newInstance() {
return new App();
}
constructor() {
if (!logger.isReady) {
logger.setup();
}
setmeup.load(__dirname + "/../settings.default.json", { overwrite: false });
settings = setmeup.settings;
}
expressApp;
server;
events = new eventemitter3_1.default();
on = (eventName, callback) => {
this.events.on(eventName, callback);
};
once = (eventName, callback) => {
this.events.on(eventName, callback);
};
off = (eventName, callback) => {
this.events.off(eventName, callback);
};
init = (middlewares) => {
let mw;
if (settings.general.debug && logger.options.levels.indexOf("debug") < 0) {
logger.setOptions({
levels: logger.options.levels.concat(["debug"])
});
}
logger.setOptions({
preprocessors: ["cleanup", "friendlyErrors", "maskSecrets"],
preprocessorOptions: {
errorStack: settings.logger.errorStack
}
});
this.expressApp = express();
const addMiddleware = (mw) => {
if (!mw)
return;
if (mw.path && mw.handler) {
this.expressApp.use(mw.path, mw.handler);
}
else {
this.expressApp.use(mw);
}
};
middlewares = middlewares || { append: [], prepend: [] };
if (middlewares.prepend && !(0, utils_1.isArray)(middlewares.prepend)) {
middlewares.prepend = [middlewares.prepend];
}
if (middlewares.append && !(0, utils_1.isArray)(middlewares.append)) {
middlewares.append = [middlewares.append];
}
if (middlewares?.prepend?.length > 0) {
for (mw of middlewares.prepend) {
addMiddleware(mw);
}
}
this.expressApp.set("trust proxy", settings.app.trustProxy);
this.expressApp.set("views", settings.app.viewPath);
if (settings.app.viewEngine) {
this.expressApp.set("view engine", settings.app.viewEngine);
this.expressApp.set("view options", settings.app.viewOptions);
}
if (settings.app.bodyParser?.enabled) {
try {
const midBodyParser = require("body-parser");
if (settings.app.bodyParser.rawTypes) {
this.expressApp.use(midBodyParser.raw({ limit: settings.app.bodyParser.rawLimit, type: settings.app.bodyParser.rawTypes }));
}
this.expressApp.use(midBodyParser.json({ limit: settings.app.bodyParser.limit }));
this.expressApp.use(midBodyParser.text({ limit: settings.app.bodyParser.limit }));
this.expressApp.use(midBodyParser.urlencoded({ limit: settings.app.bodyParser.limit, extended: settings.app.bodyParser.extended }));
}
catch (ex) {
logger.warn("App.init", "Can't load 'body-parser' module");
}
try {
const bodyParserErrorHandler = require("express-body-parser-error-handler");
this.expressApp.use(bodyParserErrorHandler());
}
catch (ex) {
logger.debug("App.init", "Module 'express-body-parser-error-handler' not installed, body-parser might throw ugly exceptions");
}
}
if (settings.app.cookie?.enabled) {
try {
const midCookieParser = require("cookie-parser");
this.expressApp.use(midCookieParser(settings.app.secret));
}
catch (ex) {
ex.friendlyMessage = "Can't load 'cookie-parser' module";
logger.error("App.init", ex);
}
}
if (settings.app.session?.enabled) {
try {
const midSession = require("express-session");
const memoryStore = require("memorystore")(midSession);
this.expressApp.use(midSession({
store: new memoryStore({ checkPeriod: settings.app.session.checkPeriod }),
proxy: settings.app.session.proxy,
resave: settings.app.session.resave,
saveUninitialized: settings.app.session.saveUninitialized,
secret: settings.app.secret,
ttl: settings.app.session.maxAge * 1000,
cookie: {
secure: settings.app.session.secure,
httpOnly: settings.app.session.httpOnly,
maxAge: settings.app.session.maxAge * 1000
}
}));
}
catch (ex) {
ex.friendlyMessage = "Can't load 'express-session' and 'memorystore' modules";
logger.error("App.init", ex);
}
}
if (settings.app.compression && settings.app.compression.enabled) {
try {
const midCompression = require("compression");
this.expressApp.use(midCompression());
}
catch (ex) {
ex.friendlyMessage = "Can't load 'compression' module";
logger.error("App.init", ex);
}
}
if (settings.app.publicPath) {
this.expressApp.use(express.static(settings.app.publicPath));
}
if (middlewares?.append?.length > 0) {
for (mw of middlewares.append) {
addMiddleware(mw);
}
}
if (settings.general.debug) {
this.expressApp.use((req, res, next) => {
const { method, url } = req;
const ip = jaul.network.getClientIP(req);
const msg = `Request from ${ip}`;
if (res) {
logger.debug("App", msg, method, url);
}
if (next) {
next();
}
return url;
});
}
if (settings.logger.errorHandler) {
this.expressApp.use((err, req, res, next) => {
logger.error("App", req.method, req.url, res.headersSent ? "Headers sent" : "Headers not sent", err);
if (err instanceof URIError) {
res.end();
}
else {
next(err);
}
});
}
this.expressApp.disable("x-powered-by");
this.events.emit("init");
this.start();
};
start = () => {
if (this.server) {
logger.warn("App.start", "Server is already running, abort start");
return this.server;
}
let serverRef;
if (settings.app.ssl?.enabled && settings.app.ssl?.keyFile && settings.app.ssl?.certFile) {
const sslKeyFile = jaul.io.getFilePath(settings.app.ssl.keyFile);
const sslCertFile = jaul.io.getFilePath(settings.app.ssl.certFile);
if (sslKeyFile && sslCertFile) {
const sslKey = fs.readFileSync(sslKeyFile, { encoding: settings.general.encoding });
const sslCert = fs.readFileSync(sslCertFile, { encoding: settings.general.encoding });
const sslOptions = { key: sslKey, cert: sslCert };
if (settings.app.http2) {
sslOptions.allowHTTP1 = true;
serverRef = http2.createSecureServer(sslOptions, this.expressApp);
}
else {
serverRef = https.createServer(sslOptions, this.expressApp);
}
}
else {
throw new Error("Invalid certificate filename, please check paths defined on settings.app.ssl");
}
}
else {
serverRef = settings.app.http2 ? http2.createServer(this.expressApp) : http.createServer(this.expressApp);
}
this.server = serverRef;
let listenCb = () => {
if (settings.app.ip) {
logger.info("App.start", settings.app.title, `Listening on ${settings.app.ip} port ${settings.app.port}`, `URL ${settings.app.url}`);
}
else {
logger.info("App.start", settings.app.title, `Listening on port ${settings.app.port}`, `URL ${settings.app.url}`);
}
};
let listenError = (err) => {
logger.error("App.start", "Can't start", err);
};
if (settings.app.ip) {
serverRef.listen(settings.app.port, settings.app.ip, listenCb).on("error", listenError);
}
else {
serverRef.listen(settings.app.port, listenCb).on("error", listenError);
}
serverRef.setTimeout(settings.app.timeout);
this.events.emit("start");
return this.server;
};
kill = () => {
if (!this.server) {
logger.warn("App.kill", "Server was not running");
return;
}
try {
this.server.close();
this.server = null;
this.events.emit("kill");
}
catch (ex) {
logger.error("App.kill", ex);
}
};
all = (...args) => {
logger.debug("App.all", args[1], args[2]);
return this.expressApp.all.apply(this.expressApp, args);
};
get = (...args) => {
logger.debug("App.get", args[1], args[2]);
return this.expressApp.get.apply(this.expressApp, args);
};
post = (...args) => {
logger.debug("App.post", args[1], args[2]);
return this.expressApp.post.apply(this.expressApp, args);
};
put = (...args) => {
logger.debug("App.put", args[1], args[2]);
return this.expressApp.put.apply(this.expressApp, args);
};
patch = (...args) => {
logger.debug("App.patch", args[1], args[2]);
return this.expressApp.patch.apply(this.expressApp, args);
};
delete = (...args) => {
logger.debug("App.delete", args[1], args[2]);
return this.expressApp.delete.apply(this.expressApp, args);
};
head = (...args) => {
logger.debug("App.head", args[1], args[2]);
return this.expressApp.head.apply(this.expressApp, args);
};
use = (...args) => {
logger.debug("App.use", args[1], args[2]);
return this.expressApp.use.apply(this.expressApp, args);
};
set = (...args) => {
logger.debug("App.set", args[1], args[2]);
return this.expressApp.set.apply(this.expressApp, args);
};
route = (reqPath) => {
logger.debug("App.route", reqPath);
return this.expressApp.route.apply(this.expressApp, reqPath);
};
renderView = (req, res, view, options, status) => {
logger.debug("App.renderView", req.originalUrl, view, options);
try {
if (!options) {
options = {};
}
if (options.title == null) {
options.title = settings.app.title;
}
if (status) {
res.status(parseInt(status));
}
res.render(view, options);
}
catch (ex) {
logger.error("App.renderView", view, ex);
this.renderError(req, res, ex);
}
if (settings.app.events.render) {
this.events.emit("renderView", req, res, view, options, status);
}
};
renderText = (req, res, text, status) => {
logger.debug("App.renderText", req.originalUrl, text);
try {
if (text == null) {
logger.debug("App.renderText", "Called with empty text parameter");
text = "";
}
else if (!(0, utils_1.isString)(text)) {
text = text.toString();
}
if (status) {
res.status(parseInt(status));
}
res.setHeader("content-type", "text/plain");
res.send(text);
}
catch (ex) {
logger.error("App.renderText", text, ex);
this.renderError(req, res, ex);
}
if (settings.app.events.render) {
this.events.emit("renderText", req, res, text, status);
}
};
renderJson = (req, res, data, status) => {
logger.debug("App.renderJson", req.originalUrl, data);
if ((0, utils_1.isString)(data)) {
try {
data = JSON.parse(data);
}
catch (ex) {
logger.error("App.renderJson", ex);
return this.renderError(req, res, ex, 500);
}
}
var cleanJson = function (obj, depth) {
if (depth >= settings.logger.maxDepth) {
return;
}
if ((0, utils_1.isArray)(obj)) {
return Array.from(obj).map((i) => cleanJson(i, depth + 1));
}
else if ((0, utils_1.isObject)(obj)) {
return (() => {
const result = [];
for (let k in obj) {
const v = obj[k];
if ((0, utils_1.isFunction)(v)) {
result.push(delete obj[k]);
}
else {
result.push(cleanJson(v, depth + 1));
}
}
return result;
})();
}
};
cleanJson(data, 0);
if (status) {
res.status(parseInt(status));
}
if (settings.app.allowOriginHeader) {
res.setHeader("Access-Control-Allow-Origin", settings.app.allowOriginHeader);
}
res.json(data);
if (settings.app.events.render) {
this.events.emit("renderJson", req, res, data, status);
}
};
renderImage = (req, res, filename, options) => {
logger.debug("App.renderImage", req.originalUrl, filename, options);
let mimetype = options ? options.mimetype : null;
if (!mimetype) {
let extname = path.extname(filename).toLowerCase().replace(".", "");
if (extname == "jpg") {
extname = "jpeg";
}
mimetype = `image/${extname}`;
}
res.type(mimetype);
res.sendFile(filename);
if (settings.app.events.render) {
this.events.emit("renderImage", req, res, filename, options);
}
};
renderError = (req, res, error, status) => {
let message;
logger.debug("App.renderError", req.originalUrl, status, error);
if (typeof error == "undefined" || error == null) {
error = "Unknown error";
logger.warn("App.renderError", "Called with null error");
}
if (status == null) {
status = error.statusCode || error.status || error.code;
}
if (status == "ETIMEDOUT") {
status = 408;
}
if (isNaN(status)) {
status = 500;
}
try {
if (error.error && !error.message && !error.error_description && !error.reason) {
error = error.error;
}
if ((0, utils_1.isString)(error)) {
message = { message: error };
}
else {
message = {};
message.message = error.message || error.error_description || error.description;
if (!message.message) {
message.message = error.toString();
}
if (error.friendlyMessage) {
message.friendlyMessage = error.friendlyMessage;
}
if (error.reason) {
message.reason = error.reason;
}
if (error.code) {
message.code = error.code;
}
else if (error.status) {
message.code = error.status;
}
}
}
catch (ex) {
logger.error("App.renderError", ex);
}
res.status(parseInt(status)).json(message);
if (settings.app.events.render) {
this.events.emit("renderError", req, res, error, status);
}
};
}
exports.App = App;
exports.default = App.Instance;