@egodigital/egoose
Version:
Helper classes and functions for Node.js 10 or later.
437 lines (435 loc) • 13.7 kB
JavaScript
"use strict";
/**
* This file is part of the @egodigital/egoose distribution.
* Copyright (c) e.GO Digital GmbH, Aachen, Germany (https://www.e-go-digital.com/)
*
* @egodigital/egoose is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, version 3.
*
* @egodigital/egoose is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
const bodyParser = require("body-parser");
const errorHandler = require("errorhandler");
const index_1 = require("../mongo/index");
const logger_1 = require("../diagnostics/logger");
const express = require("express");
const http = require("http");
const MergeDeep = require("merge-deep");
const dev_1 = require("../dev");
const index_2 = require("../index");
const util = require("util");
/**
* An API host.
*/
class ApiHost {
constructor() {
this._poweredBy = '@egodigital/egoose';
this._useBodyParser = true;
}
/**
* Gets the underlying Express app instance.
*/
get app() {
return this._app;
}
authorizer(newValue) {
if (arguments.length > 0) {
this._authorizer = newValue;
return this;
}
return this._authorizer;
}
/**
* (Re-)Initializes the host.
*
* @param {InitializeApiHostOptions} [opts] Custom options.
*/
initialize(opts) {
if (_.isNil(opts)) {
opts = {};
}
const OLD_SERVER = this._server;
if (OLD_SERVER) {
OLD_SERVER.close();
this._server = null;
}
const NEW_APP = express();
if (opts.onAppCreated) {
opts.onAppCreated
.apply(this, [NEW_APP]);
}
const NEW_LOGGER = new logger_1.Logger();
const NEW_API_ROOT = express.Router();
NEW_APP.use('/api', NEW_API_ROOT);
if (this._useBodyParser) {
let ubpOpts = {
defaultCharset: 'utf8',
inflate: true,
strict: true,
extended: true
};
if (true !== this._useBodyParser) {
ubpOpts = MergeDeep(ubpOpts, this._useBodyParser);
}
NEW_API_ROOT.use(bodyParser.json(ubpOpts));
}
const POWERED_BY = index_2.toStringSafe(this._poweredBy).trim();
if ('' !== POWERED_BY) {
NEW_API_ROOT.use((req, res, next) => {
res.header('X-Powered-By', POWERED_BY);
next();
});
}
const AUTHORIZER = this._authorizer;
if (!_.isNil(AUTHORIZER)) {
NEW_API_ROOT.use(async (req, res, next) => {
const IS_VALID = await Promise.resolve(AUTHORIZER(req));
if (IS_VALID) {
next();
return;
}
return res.status(401)
.send();
});
}
if (dev_1.IS_LOCAL_DEV) {
// trace request
NEW_API_ROOT.use((req, res, next) => {
try {
NEW_LOGGER.trace({
request: {
headers: req.headers,
method: req.method,
query: req.query,
}
}, 'request');
}
catch { }
next();
});
// only for test use
NEW_API_ROOT.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "*");
next();
});
}
this.setupLogger(NEW_LOGGER);
this._logger = NEW_LOGGER;
this.setupApi(NEW_APP, NEW_API_ROOT);
// error handler
{
let errHandlerOpts = this._useErrorHandler;
if (_.isNil(errHandlerOpts)) {
errHandlerOpts = false;
}
if (errHandlerOpts) {
if (true === errHandlerOpts) {
errHandlerOpts = {
log: (err, str, req) => {
const LOG_MSG = `Error in [${req.method}] '${req.url}':
${str}`;
this.logger
.err(LOG_MSG, 'unhandled_error');
},
};
}
NEW_API_ROOT.use(errorHandler(errHandlerOpts));
}
}
this._app = NEW_APP;
this._root = NEW_API_ROOT;
}
/**
* Gets if the host is currently running or not.
*/
get isRunning() {
return !_.isNil(this._server);
}
/**
* Gets the underlying logger.
*/
get logger() {
return this._logger;
}
poweredBy(newValue) {
if (arguments.length > 0) {
this._poweredBy = index_2.toStringSafe(newValue).trim();
return this;
}
return this._poweredBy;
}
/**
* Gets the root endpoint.
*/
get root() {
return this._root;
}
/**
* Sets a 'Basic Auth' based authorizer.
*
* @param {BasicAuthAuthorizer} authorizer The authorizer.
*
* @return this
*/
setBasicAuth(authorizer) {
return this.setPrefixedAuthorizer(async (token) => {
try {
let username;
let password;
const USERNAME_AND_PASSWORD = index_2.toStringSafe(token).trim();
if ('' !== USERNAME_AND_PASSWORD) {
const UNAME_PWD = (Buffer.from(USERNAME_AND_PASSWORD, 'base64')).toString('utf8');
const USER_PWD_SEP = UNAME_PWD.indexOf(':');
if (USER_PWD_SEP > -1) {
username = UNAME_PWD.substr(0, USER_PWD_SEP);
password = UNAME_PWD.substr(USER_PWD_SEP + 1);
}
else {
username = UNAME_PWD;
}
}
username = index_2.normalizeString(username);
password = index_2.toStringSafe(password);
return await Promise.resolve(authorizer(username, password));
}
catch { }
return false;
}, 'basic');
}
/**
* Sets a prefixed based authorizer.
*
* @param {TokenAuthorizer} authorizer The authorizer.
* @param {string} [prefix] The prefix.
*
* @return this
*/
setPrefixedAuthorizer(authorizer, prefix = 'bearer') {
prefix = index_2.normalizeString(prefix);
return this.authorizer(async (req) => {
const AUTH = index_2.toStringSafe(req.headers['authorization']).trim();
if (AUTH.toLowerCase().startsWith(prefix + ' ')) {
return await Promise.resolve(authorizer(AUTH.substr(prefix.length + 1)));
}
return false;
});
}
/**
* Sets up a new api / app instance.
*
* @param {express.Express} newApp The instance to setup.
* @param {express.Router} newRoot The API root.
*/
setupApi(newApp, newRoot) {
}
/**
* Sets up a new logger instance.
*
* @param {Logger} newLogger The instance to setup.
*/
setupLogger(newLogger) {
}
/**
* Starts the host.
*
* @param {number} [port] The custom port to use. By default 'APP_PORT' environment variable is used.
* Otherwise 80 is the default port.
*
* @return {Promise<boolean>} The promise, which indicates if operation successful or not.
*/
start(port) {
if (arguments.length < 1) {
port = parseInt(index_2.toStringSafe(process.env.APP_PORT).trim());
}
else {
port = parseInt(index_2.toStringSafe(port).trim());
}
return new Promise((resolve, reject) => {
if (this.isRunning) {
resolve(false);
return;
}
try {
let serverFactory;
// TODO: implement secure HTTP support
serverFactory = () => {
if (isNaN(port)) {
port = 80;
}
return http.createServer(this.app);
};
const NEW_SERVER = serverFactory();
NEW_SERVER.listen(port, () => {
this._server = NEW_SERVER;
resolve(true);
});
}
catch (e) {
reject(e);
}
});
}
/**
* Stops the host.
*
* @return {Promise<boolean>} The promise, which indicates if operation successful or not.
*/
stop() {
return new Promise((resolve, reject) => {
const OLD_SERVER = this._server;
if (_.isNil(OLD_SERVER)) {
resolve(false);
return;
}
try {
OLD_SERVER.close(() => {
this._server = null;
resolve(true);
});
}
catch (e) {
reject(e);
}
});
}
useBodyParser(newValue) {
if (arguments.length > 0) {
this._useBodyParser = newValue;
return this;
}
return this._useBodyParser;
}
useErrorHandler(newValue) {
if (arguments.length > 0) {
this._useErrorHandler = newValue;
return this;
}
return this._useErrorHandler;
}
}
exports.ApiHost = ApiHost;
/**
* An API with MongoDB helper methods.
*/
class MongoApiHost extends ApiHost {
/**
* Returns the database class.
*
* @return {MongoDatabase} The class.
*/
getDatabaseClass() {
return index_1.MongoDatabase;
}
/**
* Log something into the database.
* This requires a 'logs' collection, described by 'LogsDocument' interface.
*
* @param {any} message The message.
* @param {LogType} type The type.
* @param {any} [payload] The (optional) payload.
*/
async log(message, type, payload) {
const NOW = index_2.utc();
try {
await this.withDatabase(async (db) => {
await db.model('Logs').insertMany([{
created: NOW.toDate(),
message: toSerializableLogValue(message),
payload: toSerializableLogValue(payload),
type: type,
uuid: index_2.guid(),
}]);
});
}
catch (error) {
console.log('logging failed', error);
}
}
/**
* Options a new connection to a database.
*
* @param {TOptions} [opts] The custom options to use.
*
* @return {TDatabase} The new, opened, database.
*/
async openDatabase(opts) {
const DB_CLASS = this.getDatabaseClass();
let db;
if (_.isNil(opts)) {
// use environment variables
db = new DB_CLASS({
database: process.env.MONGO_DB,
host: process.env.MONGO_HOST,
options: process.env.MONGO_OPTIONS,
port: parseInt(process.env.MONGO_PORT),
password: process.env.MONGO_PASSWORD,
user: process.env.MONGO_USER,
});
}
else {
db = new DB_CLASS(opts);
}
await db.connect();
return db;
}
/**
* Opens a data connection and invokes an action for it.
* After invokation, the database is closed automatically.
*
* @param {Function} action The action to invoke.
* @param {TOptions} [opts] Custom database options.
*
* @return {Promise<TResult>} The promise with the result of the action.
*/
async withDatabase(action, opts) {
const DB = await this.openDatabase(opts);
try {
return await Promise.resolve(action(DB));
}
finally {
await DB.disconnect();
}
}
}
exports.MongoApiHost = MongoApiHost;
function toSerializableLogValue(value) {
if (_.isNil(value)) {
value = null;
}
else {
if (Array.isArray(value) || _.isObjectLike(value)) {
try {
value = util.inspect(value, {
depth: null,
maxArrayLength: 512,
showHidden: false,
});
}
catch (e) {
value = index_2.toStringSafe(value)
.trim();
}
}
else if (value instanceof Error) {
value = `(ERR::${value.name}) '${value.message}'
${value.stack}`;
}
else {
value = index_2.toStringSafe(value)
.trim();
}
}
return value;
}
//# sourceMappingURL=host.js.map