UNPKG

@egodigital/egoose

Version:

Helper classes and functions for Node.js 10 or later.

437 lines (435 loc) 13.7 kB
"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