framework/twyr-feature-api-service.js

'use strict';

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

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

/**
 * Module dependencies, required for this module
 * @ignore
 */
const TwyrBaseService = require('twyr-base-service').TwyrBaseService;
const TwyrSrvcError = require('twyr-service-error').TwyrServiceError;

/**
 * @class   FeatureApiService
 * @extends {TwyrBaseService}
 * @classdesc The Twyr Web Application Server API Service.
 *
 * @description
 * Allows the modules in a single feature to communicate with each other by allowing registration / invocation of API.
 * If the requested for API cannot be found, it bubbles up to the next level, till it gets to the server
 *
 */
class FeatureApiService extends TwyrBaseService {
	// #region Constructor
	constructor(parent, loader) {
		super(parent, loader);
	}
	// #endregion

	// #region startup/teardown code
	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof FeatureApiService
	 * @name     _setup
	 *
	 * @returns  {null} Nothing.
	 *
	 * @summary  Sets up the broker to manage API exposed by other modules.
	 */
	async _setup() {
		try {
			await super._setup();

			const customMatch = function(pattern, data) {
				const items = this.find(pattern, true) || [];
				items.push(data);

				return {
					'find': function() {
						return items.length ? items : [];
					},

					'remove': function(search, api) {
						const apiIdx = items.indexOf(api);
						if(apiIdx < 0) return false;

						items.splice(apiIdx, 1);
						return true;
					}
				};
			};

			this.$patrun = require('patrun')(customMatch);
			return null;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::_setup error`, err);
		}
	}

	/**
	 * @async
	 * @function
	 * @override
	 * @instance
	 * @memberof FeatureApiService
	 * @name     _teardown
	 *
	 * @returns  {undefined} Nothing.
	 *
	 * @summary  Deletes the broker that manages API.
	 */
	async _teardown() {
		try {
			if(this.$patrun) delete this.$patrun;

			await super._teardown();
			return null;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::_teardown error`, err);
		}
	}
	// #endregion

	// #region API
	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof FeatureApiService
	 * @name     add
	 *
	 * @param    {string} pattern - The pattern to which this api will respond.
	 * @param    {Function} api - The api to be invoked against the pattern.
	 *
	 * @returns  {boolean} Boolean true/false - depending on whether the registration succeeded.
	 *
	 * @summary  Registers the api function as a handler for the pattern.
	 */
	async add(pattern, api) {
		try {
			// eslint-disable-next-line curly
			if(typeof api !== 'function') {
				throw new Error(`${this.name}::add expects a function for the pattern: ${pattern}`);
			}

			pattern = pattern.split('::').reduce((obj, value) => {
				obj[value] = value;
				return obj;
			}, {});

			this.$patrun.add(pattern, api);
			return true;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::add error`, err);
		}
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof FeatureApiService
	 * @name     execute
	 *
	 * @param    {string} pattern - The pattern to be executed.
	 * @param    {Object} data - The data to be passed in as arguments to each of the api registered against the pattern.
	 *
	 * @returns  {Array} The results of the execution.
	 *
	 * @summary  Executes all the apis registered as handlers for the pattern.
	 */
	async execute(pattern, data) {
		try {
			const patrunPattern = pattern.split('::').reduce((obj, value) => {
				obj[value] = value;
				return obj;
			}, {});

			if(!Array.isArray(data))
				data = [data];

			const apis = this.$patrun.find(patrunPattern);
			if(!apis || !apis.length) {
				let parentModule = this.$parent;
				let parentApiService = parentModule.$dependencies['ApiService'];

				while(!parentApiService) {
					parentApiService = parentModule.$dependencies['ApiService'];
					if(!parentApiService) parentModule = parentModule.$parent;
					if(!parentModule) throw new TwyrSrvcError(`No API Service found to execute request`);
				}

				const parentResults = await parentApiService.execute(pattern, data);
				return parentResults;
			}

			const results = [];

			let errors = null;
			for(const api of apis) { // eslint-disable-line curly
				try {
					const result = await api(...data);
					results.push(result);
				}
				catch(execErr) {
					if(!errors)
						errors = new TwyrSrvcError(execErr);
					else
						errors = new TwyrSrvcError(execErr, errors);
				}
			}

			if(!errors)
				return results;
			else
				throw errors;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::execute error`, err);
		}
	}

	/**
	 * @async
	 * @function
	 * @instance
	 * @memberof FeatureApiService
	 * @name     remove
	 *
	 * @param    {string} pattern - The pattern to which this api will respond.
	 * @param    {Function} api - The api to be de-registered against the pattern.
	 *
	 * @returns  {boolean} Boolean true/false - depending on whether the de-registration succeeded.
	 *
	 * @summary  De-registers the api function as a handler for the pattern.
	 */
	async remove(pattern, api) {
		try {
			// eslint-disable-next-line curly
			if(typeof api !== 'function') {
				throw new Error(`${this.name}::remove expects a function for the pattern: ${pattern}`);
			}

			pattern = pattern.split('::').reduce((obj, value) => {
				obj[value] = value;
				return obj;
			}, {});

			this.$patrun.remove(pattern, api);
			return true;
		}
		catch(err) {
			throw new TwyrSrvcError(`${this.name}::remove error`, err);
		}
	}
	// #endregion

	// #region Properties
	/**
	 * @override
	 */
	get Interface() {
		return {
			'add': this.add.bind(this),
			'execute': this.execute.bind(this),
			'remove': this.remove.bind(this)
		};
	}

	/**
	 * @override
	 */
	get dependencies() {
		return ['ConfigurationService', 'LoggerService'].concat(super.dependencies);
	}

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

exports.service = FeatureApiService;