@breautek/storm
Version:
Object-Oriented REST API framework
432 lines (429 loc) • 15 kB
JavaScript
"use strict";
/*
Copyright 2017-2021 Norman Breau
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Application = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const instance_1 = require("./instance");
const ApplicationEvent_1 = require("./ApplicationEvent");
const Request_1 = require("./Request");
const Response_1 = require("./Response");
const ConfigLoader_1 = require("./ConfigLoader");
const commander_1 = require("commander");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Express = require("express");
const BodyParser = tslib_1.__importStar(require("body-parser"));
const http = tslib_1.__importStar(require("http"));
const Path = tslib_1.__importStar(require("path"));
const logger_1 = require("@arashi/logger");
const StormError_1 = require("./StormError");
const TAG = 'Application';
/**
* Main entry point for the Application. Should be extended and have the abstract methods implemented.
*/
class Application extends events_1.EventEmitter {
/**
*
* @param name The application name
* @param configPath @deprecated The directory where bt-config.json and bt-local-config.json can be found. Defaults to current working directory.
*/
constructor(name, configPath) {
super();
(0, instance_1.setInstance)(this);
this.$buildArgOptions();
this.$usingDeprecatedConfigPath = !!configPath;
this.$program.parse(process.argv);
this.$name = name;
process.on('unhandledRejection', (error) => {
try {
this.$getLogger().error(TAG, error);
}
catch (ex) {
console.error('Unhandled Exception:', ex);
}
});
this.$configDir = configPath || process.cwd();
}
async start() {
this.$logger = new logger_1.Logger(this.constructor.name);
this.$logger.info(TAG, 'Application is booting...');
this.$logger.info(TAG, 'Loading Configuration...');
if (this.$usingDeprecatedConfigPath) {
this.$getLogger().warn(TAG, `configPath constructor argument is deprecated. Use --config and --local_config instead.`);
}
try {
await this.$load();
}
catch (error) {
this.$getLogger().error(TAG, error);
}
}
async $load() {
this.$config = await this.$loadConfig();
// console.log('WTF', this.$config);
this.$logger = await this._initLogger(this.$config);
this.$getLogger().trace(TAG, 'Configuration loaded.');
this.emit(ApplicationEvent_1.ApplicationEvent.CONFIG_LOADED);
this._onConfigLoad(this.$config);
this.$getLogger().trace(TAG, 'Initializing DB...');
this.$db = await this._initDB(this.getConfig());
if (this.$db) {
this.$getLogger().trace(TAG, 'DB Initialized.');
}
else {
this.$getLogger().trace(TAG, 'DB is not initialized.');
}
this.$getLogger().trace(TAG, 'Starting server...');
this.$server = Express();
this.$server.use(BodyParser.json({
type: 'application/json',
limit: this.getRequestSizeLimit()
}));
this.$server.use(BodyParser.text({
type: 'text/*',
limit: this.getRequestSizeLimit()
}));
this.$getLogger().trace(TAG, 'Attaching handlers...');
await this._attachHandlers();
await this._onBeforeReadyAsync();
await new Promise((resolve, reject) => {
let bindingIP = this.getConfig().bind;
let port = this.getConfig().port;
if (bindingIP !== null && bindingIP !== 'null') {
if (this.shouldListen()) {
this.$socket = http.createServer(this.$server);
this.$socket.listen(port, bindingIP, () => {
this.$getLogger().trace(TAG, `Server started on ${bindingIP}:${this.getPort()}`);
resolve();
});
}
else {
this.$getLogger().trace(TAG, 'Server did not bind because shouldListen() returned false.');
resolve();
}
}
else {
this.$getLogger().info(TAG, `Server does not have a bounding IP set. The server will not be listening for connections.`);
resolve();
}
});
await this._initialize(this.getConfig());
this._onReady();
this.emit('ready');
}
_initialize(config) {
return Promise.resolve();
}
_createLogger(config) {
return new logger_1.Logger(this.getName(), config.log?.level);
}
async _initLogger(config) {
let logger = this._createLogger(config);
if (config?.log?.cloudwatch) {
let cwConfig = config.log.cloudwatch;
let cwCheck = this.$validateCWConfig(cwConfig);
if (cwCheck === null) {
await this.$connectCW(logger, cwConfig);
}
else {
logger.warn(TAG, `Skipped configuration cloudwatch: ${cwCheck}`);
}
}
return logger;
}
async $connectCW(logger, cwConfig) {
let cw = await logger_1.CloudWatchStream.create({
region: cwConfig.region,
credentials: {
accessKeyId: cwConfig.credentials.accessKeyId,
secretAccessKey: cwConfig.credentials.secretAccessKey
}
}, {
group: cwConfig.stream.group,
stream: cwConfig.stream.name
});
logger.pipe(cw);
}
$validateCWConfig(config) {
const BASE = 'missing $.log.cloudwatch.';
if (!config.region) {
return BASE + 'region';
}
if (!config.credentials) {
return BASE + 'credentials';
}
if (!config.credentials.accessKeyId) {
return BASE + 'credentials.accessKeyId';
}
if (!config.credentials.secretAccessKey) {
return BASE + 'credentials.secretAccessKey';
}
if (!config.stream) {
return BASE + 'stream';
}
if (!config.stream.group) {
return BASE + 'stream.group';
}
if (!config.stream.name) {
return BASE + 'stream.name';
}
return null;
}
getLogger() {
return this.$logger;
}
getPort() {
let port = null;
if (this.$socket && this.$socket.listening) {
let address = this.$socket.address();
if (typeof address !== 'string') {
port = address.port;
}
}
return port;
}
getVersion() {
return this._getVersion();
}
_getVersion() {
console.warn(TAG, `_getVersion will be an abstract method in the next major release. Override this method and pass a version string.`);
return '0.0.0';
}
$getVersionString() {
// eslint-disable-next-line @typescript-eslint/no-require-imports
let pkg = require('../package.json');
return `${this._getVersion()} (Storm ${pkg.version})`;
}
$buildArgOptions() {
this.$program = new commander_1.Command();
this.$program.version(this.$getVersionString(), '-v, --version');
this.$program.allowUnknownOption(true);
this.$program.allowExcessArguments(true);
this.$program.option('--port <port>', 'The running port to consume');
this.$program.option('--bind <ip>', 'The binding IP to listen on');
this.$program.option('--authentication_header <header>', 'The header name of the authentication token');
this.$program.option('--config <path>', 'The path to the bt-config.json file');
this.$program.option('--local-config <path>', 'The path to the bt-local-config.json file.');
this._buildArgOptions(this.$program);
}
_buildArgOptions(program) { }
getProgram() {
return this.$program;
}
/**
* Override this method to map CLI args to customConfig
* @param args
*/
getConfigFromCLIArgs(args) {
return {};
}
/**
* The maximum size limit for incoming requests that this service needs to handle.
*/
getRequestSizeLimit() {
return this.getConfig().request_size_limit;
}
/**
*
* @param path The URL API path. E.g. /api/myService/myCommand/
* @param HandlerClass The concrete class (not the instance) of Handler to be used for this API.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
attachHandler(path, HandlerClass) {
let handler = new HandlerClass(this);
this.attachHandlerInstance(path, handler);
}
attachHandlerInstance(path, handler) {
this.$server.get(path, (request, response) => {
let r = new Request_1.Request(request);
handler.get(r, new Response_1.Response(response, r.getURL()));
});
this.$server.post(path, (request, response) => {
let r = new Request_1.Request(request);
handler.post(r, new Response_1.Response(response, r.getURL()));
});
this.$server.put(path, (request, response) => {
let r = new Request_1.Request(request);
handler.put(r, new Response_1.Response(response, r.getURL()));
});
this.$server.delete(path, (request, response) => {
let r = new Request_1.Request(request);
handler.delete(r, new Response_1.Response(response, r.getURL()));
});
}
async close() {
await Promise.all([this._closeSocket(), this._closeDatabase()]);
if (this.$logger) {
this.$logger.destroy();
}
}
async _closeDatabase() {
if (this.$db) {
await this.$db.destroy();
}
}
async _closeSocket() {
return new Promise((resolve, reject) => {
if (this.$socket && this.$socket.listening) {
this.$socket.close(() => {
resolve();
});
}
else {
resolve();
}
});
}
/**
* @deprecated Supply the configs via --config and --local-config arguments
*
* @param path The directory path that contains bt-config.json and bt-local-config.json
*/
loadConfig(path) {
return new Promise((resolve, reject) => {
ConfigLoader_1.ConfigLoader.load(path).then((config) => {
resolve(config);
}).catch((error) => {
if (error instanceof StormError_1.StormError) {
if (error.getExitCode() !== null) {
process.exit(error.getExitCode());
}
}
else {
this.$getLogger().error(TAG, error);
}
});
});
}
async $loadConfig() {
let loader = new ConfigLoader_1.ConfigLoader(this);
return await loader.load(this.$getConfigFilePath(), this.$getLocalConfigFilePath());
}
getConfigFilePath() {
return this.$getConfigFilePath();
}
getLocalConfigFilePath() {
return this.$getLocalConfigFilePath();
}
$getLocalConfigFilePath() {
if (!this.$localConfigPath) {
let filePath = this.getCmdLineArgs().localConfigFile;
if (!filePath) {
filePath = Path.resolve(this.$configDir, 'bt-local-config.json');
}
this.$localConfigPath = Path.resolve(filePath);
}
return this.$localConfigPath;
}
$getConfigFilePath() {
if (!this.$configPath) {
let filePath = this.getCmdLineArgs().configFile;
if (!filePath) {
filePath = Path.resolve(this.$configDir, 'bt-config.json');
}
this.$configPath = Path.resolve(filePath);
}
return this.$configPath;
}
/**
* @returns the application name
*/
getName() {
return this.$name;
}
$getLogger() {
return this.$logger;
}
/**
* @returns the config object.
*/
getConfig() {
return this.$config;
}
/**
* @returns true if the Application should bind to an IP address
*/
shouldListen() {
return true;
}
/**
* Invoked once the config has been loaded and ready to be used.
*
* @param config The config object (as defined in bt-config.json/bt-local-config.json)
*/
_onConfigLoad(config) { }
/**
* Sets the TokenManager to be used for authentication.
* @param tokenManager
*/
setTokenManager(tokenManager) {
this.$tokenManager = tokenManager;
}
/**
* @returns the token manager
*/
getTokenManager() {
return this.$tokenManager;
}
/**
* @returns the database pool. This will need to be casted based on your preferred database dialect.
*/
getDB() {
return this.$db;
}
/**
* @returns command line arguments
*/
getCmdLineArgs() {
let program = this.$program;
let o = {
custom: null
};
if (!program) {
return o;
}
let opts = program.opts();
o.custom = opts;
if (opts.bind !== undefined) {
o.bind = opts.bind;
}
if (opts.port !== undefined) {
o.port = parseInt(opts.port);
}
if (opts.authentication_header !== undefined) {
o.authentication_header = opts.authentication_header;
}
if (opts.config !== undefined) {
o.configFile = opts.config;
}
if (opts.localConfig !== undefined) {
o.localConfigFile = opts.localConfig;
}
return o;
}
/**
* Subclasses are expected to override this to configure their database setup, if the service uses a database.
* @param config The bt-config object
*/
_initDB(config) {
return Promise.resolve(null);
}
async _onBeforeReadyAsync() { }
/**
* Invoked when the application is considered ready for operation.
*/
_onReady() { }
}
exports.Application = Application;
//# sourceMappingURL=Application.js.map