server/twyr-application-class.js

/* eslint-disable security/detect-object-injection */

'use strict';

/**
 * Module dependencies, required for ALL Twyr' modules
 * @ignore
 */

/**
 * Module dependencies, required for this module
 * @ignore
 */
const upash = require('upash');
upash.install('pbkdf2', require('@phc/pbkdf2'));

const TwyrBaseModule = require('twyr-base-module').TwyrBaseModule;
const TwyrBaseError = require('twyr-base-error').TwyrBaseError;

/**
 * @class   TwyrApplication
 * @extends {TwyrBaseModule}
 *
 * @param   {TwyrBaseModule} [parent] - The parent module, if any. In this case, it's always null
 * @param   {TwyrModuleLoader} [loader] - The loader to be used for managing modules' lifecycle, if any.
 *
 * @classdesc The Twyr Server Application Class.
 *
 * @description
 * The Application Class for this server.
 *
 * This class has two major functionalities:
 * 1. Provide an environment for the rest of the server modules to reside in
 * +  Load / Unload the server modules and control the Startup / Shutdown sequences
 */
class TwyrApplication extends TwyrBaseModule {
	// #region Constructor
	constructor(application, parent, loader) {
		super(parent, loader);

		this.$application = application;
		this.$uuid = require('uuid/v4')();
	}
	// #endregion

	// #region Startup / Shutdown
	/**
	 * @function
	 * @instance
	 * @memberof TwyrApplication
	 * @name     bootupServer
	 *
	 * @returns  {Array} - The aggregated status returned by sub-modules (if any).
	 *
	 * @summary  Loads / Initializes / Starts-up sub-modules.
	 */
	async bootupServer() {
		if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::bootupServer`);

		const allStatuses = [];
		let bootupError = null;

		try {
			const osLocale = require('os-locale');
			const serverLocale = await osLocale();

			Object.defineProperties(this, {
				'$locale': {
					'value': serverLocale
				}
			});

			let lifecycleStatuses = null;

			this.emit('server-loading');
			lifecycleStatuses = await this.load();
			allStatuses.push(`${process.title} load status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-loaded');

			this.emit('server-initializing');
			lifecycleStatuses = await this.initialize();
			allStatuses.push(`${process.title} initialize status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-initialized');

			this.emit('server-starting');
			lifecycleStatuses = await this.start();
			allStatuses.push(`${process.title} start status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-started');

			await this._setupWebserverRoutes();
			this.emit('server-online');
		}
		catch(err) {
			bootupError = (err instanceof TwyrBaseError) ? err : new TwyrBaseError(`Bootup Error`, err);
			allStatuses.push(`Bootup error: ${bootupError.toString()}`);
		}

		if(!bootupError && ((twyrEnv === 'development') || (twyrEnv === 'test')))
			console.info(`\n\n${allStatuses.join('\n')}\n\n`);

		if(bootupError) {
			console.error(`\n\n${allStatuses.join('\n')}\n\n`);
			throw bootupError;
		}

		return null;
	}

	/**
	 * @function
	 * @instance
	 * @memberof TwyrApplication
	 * @name     shutdownServer
	 *
	 * @returns  {Array} - The aggregated status returned by sub-modules (if any).
	 *
	 * @summary  Shuts-down / Un-initializes / Un-loads sub-modules.
	 */
	async shutdownServer() {
		if(twyrEnv === 'development' || twyrEnv === 'test') console.log(`${this.name}::shutdownServer`);

		const allStatuses = [];
		let shutdownError = null;

		try {
			this.emit('server-offline');
			let lifecycleStatuses = null;

			this.emit('server-stopping');
			lifecycleStatuses = await this.stop();
			allStatuses.push(`${process.title} stop status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-stopped');

			this.emit('server-uninitializing');
			lifecycleStatuses = await this.uninitialize();
			allStatuses.push(`${process.title} uninitialize status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-uninitialized');

			this.emit('server-unloading');
			lifecycleStatuses = await this.unload();
			allStatuses.push(`${process.title} unload status: ${lifecycleStatuses ? JSON.stringify(lifecycleStatuses, null, 2) : true}\n`);
			this.emit('server-unloaded');
		}
		catch(err) {
			shutdownError = (err instanceof TwyrBaseError) ? err : new TwyrBaseError(`Shutdown Error`, err);
			allStatuses.push(`Shutdown error: ${shutdownError.toString()}`);
		}

		if(!shutdownError && ((twyrEnv === 'development') || (twyrEnv === 'test')))
			console.info(`\n\n${allStatuses.join('\n')}\n\n`);

		if(shutdownError) {
			console.error(`\n\n${allStatuses.join('\n')}\n\n`);
			throw shutdownError;
		}

		return null;
	}
	// #endregion

	// #region Re-configuration
	async _subModuleReconfigure(subModule) {
		await super._subModuleReconfigure(subModule);
		if(subModule.name === 'WebserverService') {
			await this._setupWebserverRoutes();
			return;
		}

		const subModuleType = Object.getPrototypeOf(Object.getPrototypeOf(subModule)).name;
		if((subModuleType !== 'TwyrBaseComponent') && (subModuleType !== 'TwyrBaseFeature'))
			return;

		const configService = this.$services.ConfigurationService;
		const webserverService = this.$services.WebserverService;

		const webserverConfig = await configService.loadConfiguration(webserverService);
		await webserverService._reconfigure(webserverConfig.configuration);
	}
	// #endregion

	// #region Private Methods
	async _setupWebserverRoutes() {
		const appRouter = this.$services.WebserverService.Interface.Router;

		// Browser Error Data via the Beacon API / XHR Post
		appRouter.post('/collectClientErrorData', (ctxt) => {
			const query = ctxt.query;
			const beaconData = Object.assign({}, query, ctxt.request.body);

			ctxt.status = 200;
			ctxt.body = { 'status': true };

			if(beaconData.error === 'TransitionAborted')
				return;

			// TODO: Do something more sophisticated - like storing it into Cassandra, and running analysis
			console.error(`Client Error Data: ${JSON.stringify(beaconData, null, '\t')}`);
		});

		// Add in the components
		Object.keys(this.$components || {}).forEach((componentName) => {
			const componentRouter = this.$components[componentName].Router;
			appRouter.use(componentRouter.routes());
		});

		// Add in the features
		Object.keys(this.$features || {}).forEach((featureName) => {
			const featureRouter = this.$features[featureName].Router;
			appRouter.use(featureRouter.routes());
		});

		// Add in the templates at the end...
		Object.keys(this.$templates).forEach((tmplName) => {
			const tmplRouter = this.$templates[tmplName].Router;
			appRouter.get('*', tmplRouter.routes());
		});

		// console.log(`All Routes: ${JSON.stringify(appRouter.stack.map((route) => { return `${JSON.stringify(route.methods)} ${route.path}`; }), null, '\t')}`);
		return;
	}
	// #endregion

	// #region Properties
	/**
	 * @member   {string} name
	 * @instance
	 * @override
	 * @memberof TwyrApplication
	 *
	 * @readonly
	 */
	get name() {
		return this.$application || this.constructor.name;
	}

	/**
	 * @override
	 */
	get basePath() {
		return __dirname;
	}
	// #endregion
}

exports.TwyrApplication = TwyrApplication;