ksmf
Version:
Modular Microframework for create minimalistic CLI/Web application or REST API
386 lines (358 loc) • 12.3 kB
JavaScript
/**
* @author Antonio Membrides Espinosa
* @email tonykssa@gmail.com
* @date 15/11/2021
* @copyright Copyright (c) 2020-2030
* @license GPL
* @version 1.0
**/
const _path = require('path');
const dotenv = require('dotenv');
const KsDp = require('ksdp');
const Config = require('../common/Config');
const Dir = require('../common/Dir');
class App {
/**
* @type {import('ksdp').integration.IoC}
*/
helper;
/**
* @type {import('ksdp').behavioral.Emitter}
*/
srvEvent;
/**
* @type {Config|null}
*/
srvConfig = null;
/**
* @type {Dir|null}
*/
srvDir = null;
/**
* @type {Console|null}
*/
logger = null;
/**
* @type {Array}
*/
mod;
/**
* @type {Object}
*/
cfg;
/**
* @description initialize library
* @param {Object} [option]
* @param {String} [option.path] project root path
* @param {Object} [option.config] configuration options
* @param {Object} [option.srvHelper] driver to manage plugins
* @param {Object} [option.srvEvent] driver to manage events
* @param {Object} [option.srvConfig] driver to manage configurations
* @param {Object} [option.srvDir] driver to manage directories
* @param {Array<any>} [option.mod] plugins/modules list
**/
constructor(option = null) {
this.mod = option?.mod || [];
this.cfg = { srv: option?.config };
this.path = _path.resolve(option?.path || '../../../../');
this.helper = option?.srvHelper || new KsDp.integration.IoC.cls.Async();
this.srvEvent = option?.srvEvent || new KsDp.behavioral.Emitter();
this.srvConfig = option?.srvConfig || new Config();
this.srvDir = option?.srvDir || new Dir();
}
/**
* @description start application
* @param {import('../types').TAppConfig} [options]
*/
async start(options = null) {
this.emit('onStart', [options, this]);
}
/**
* @description stop application
* @param {import('../types').TAppConfig} [options]
*/
async stop(options = null) {
this.emit('onStop', [options, this]);
}
/**
* @description register a plugin
* @param {Object|String|Function|Array} plugin
* @param {Object} [option]
* @returns {App} self
*/
register(plugin, option = 'default') {
if (!plugin || !this.helper) {
return this;
}
this.helper.set(plugin, option);
plugin.helper = this.helper;
plugin.init instanceof Function && plugin.init();
return this;
}
/**
* @description remove a plugin
* @param {Object|String|Function|Array} plugin
* @param {Object} option
* @returns {App} self
*/
unregister(plugin = 'default', option = null) {
if (!plugin || !this.helper) {
return this;
}
this.helper.del(plugin, option);
return this;
}
/**
* @description add listener to event
* @param {Array|Object|Function} subscriber
* @param {String} [event]
* @param {Object} [option]
* @param {String} [option.event]
* @param {String} [option.scope]
* @param {Number} [option.index]
* @param {Array} [option.rows]
* @return {App} self
*/
subscribe(subscriber, event, option = null) {
this.srvEvent?.subscribe(subscriber, event, option);
return this;
}
/**
* @description remove listener from event
* @param {String} event
* @param {Object} [option]
* @param {Number} [option.index]
* @param {String} [option.event]
* @param {String} [option.scope]
* @param {Number} [option.count]
* @param {Array} [option.rows]
* @param {Array|Object|Function} [subscriber]
* @return {App} self
*/
unsubscribe(event, option = null, subscriber = null) {
this.srvEvent?.unsubscribe(event, subscriber, option);
return this;
}
/**
* @description safely trigger events
* @param {String} event
* @param {Array} params
* @returns {App} self
*/
emit(event, params = []) {
try {
this.srvEvent?.emit instanceof Function && this.srvEvent.emit(event, ...params);
}
catch (error) {
this.logger?.error instanceof Function && this.logger.error({
src: 'KsMf:App:emit',
error
});
}
return this;
}
/**
* @description initialize the application
* @param {import('../types').TAppConfig} [options]
* @returns {Promise<App>} self
*/
init(options) {
try {
this.initLoad(options);
this.initConfig(options);
} catch (error) {
this.emit('onError', [error, this]);
}
return Promise.resolve(this);
}
/**
* @description preload configuration file, variables, environments, etc
* @param {import('../types').TAppConfig} [options]
*/
initLoad(options) {
dotenv.config();
const env = process.env || {};
const flc = env["CFG_FILE"] || 'cfg/core.json';
const eid = env["NODE_ENV"] || 'development';
const loc = this.srvConfig.load('cfg/core.json', { dir: _path.resolve(__dirname, '../../'), id: eid });
const srv = options?.config || this.cfg?.srv || this.srvConfig.load(flc, { dir: this.path, id: eid });
const pac = this.srvConfig.load(_path.join(this.path, 'package.json'));
this.cfg.env = env;
this.cfg.eid = eid;
this.cfg.srv = { ...loc, ...srv };
this.cfg.srv.helper = { ...loc.helper, ...srv?.helper };
this.cfg.path = this.path;
this.cfg.pack = pac;
}
/**
* @description initialize configurations
* @param {import('../types').TAppConfig} [options]
*/
async initConfig(options) {
this.cfg.srv.module = options?.module || this.cfg.srv.module || {};
this.cfg.srv.module.path = options?.module?.path || _path.join(this.path, 'src/');
this.cfg.srv.module.path = _path.resolve(this.cfg.srv.module.path.replace('./', this.path));
this.cfg.srv.log = this.cfg.env.LOG_LEVEL ? this.cfg.env.LOG_LEVEL : this.cfg.srv.log;
this.cfg.srv.event = this.cfg.srv.event || {};
this.cfg.srv.doc = this.cfg.srv.doc || {};
// ... configure Helper ...
this.helper.configure({
path: this.cfg.srv.module.path,
src: this.cfg.srv.helper,
name: 'helper',
error: {
on: (error) => this.emit('onError', [error, this])
}
});
await this.helper.set(this, 'app');
// ... configure Events ...
await this.initEvents();
this.emit('onInitConfig', [this.cfg, this]);
return this;
}
/**
* @description initialize event handler
*/
async initEvents() {
for (let event in this.cfg.srv.event) {
let eventList = this.cfg.srv.event[event];
for (let elm in eventList) {
let subscriber = eventList[elm];
if (subscriber && this.srvEvent?.add instanceof Function) {
try {
let handler = await this.helper.get(subscriber);
handler && this.srvEvent.add(handler, event);
}
catch (error) {
this.logger?.error({
src: 'KsMf:App:initEvents',
error
});
}
}
}
}
return this;
}
/**
* @description load modules
*/
async initModules() {
this.emit('onInitModules', [this.cfg.srv.module.load, this]);
const modules = [];
const mode = this.cfg?.srv?.module?.mode;
if (this.cfg?.srv?.module?.load) {
this.cfg.srv.module.load.forEach(item => this.initModule(item, modules));
}
if (mode === "auto" || mode === "dev") {
const option = { watchRecursive: mode === "dev", readRecursive: false, onlyDir: true };
const modDir = _path.resolve(this.cfg?.srv?.module?.path || _path.join(this.path, 'src'));
await this.srvDir.on(modDir, async (item) => {
if (item.name) {
try {
await this.initModule(item.name, modules);
}
catch (error) {
this.logger?.error({
src: 'KsMf:App:initModule',
error
});
}
}
}, option);
}
this.emit('onLoadedModules', [modules, this]);
this.modules = modules;
return this;
}
/**
* @description initialize a module
* @param {import('../types').TOption|String} item
* @param {Array} modules
* @returns {Promise<Object>} module
*/
async initModule(item, modules) {
const name = (typeof (item) === 'string') ? item : item.name;
const options = {
// ... EXPRESS APP
app: this,
// ... extraoptions
...this.initModuleOpts(),
// ... DATA ACCESS Object
opt: {
// ... CONFIGURE
'cfg': this.cfg.srv,
// ... ENV
'env': this.cfg.env,
'eid': this.cfg.eid,
// ... PATH
'path': {
'prj': _path.resolve(this.path),
'mod': _path.join(this.cfg.srv.module.path, name),
'app': _path.join(this.cfg.srv.module.path, "app")
},
// ... NAME
'name': name,
'prefix': this.cfg.srv?.prefix || ""
}
};
const dependency = { 'helper': 'helper' };
if (typeof (item) === 'string') {
item = {
options,
dependency,
name,
type: 'module'
};
} else {
item.options = {
...item.options,
...item.params,
...options
};
item.dependency = {
...item.dependency,
...dependency
};
}
item.mode = 'transient';
// load common plugins
let obj = await this.helper.get(item);
// load npm plugins
if (!obj && this.cfg?.srv?.module?.npm) {
item.type = 'lib';
obj = await this.helper.get(item);
}
// load default plugins
if (!obj && this.cfg?.srv?.module?.default) {
let cfg = await this.helper.get({ file: _path.join(this.cfg?.srv?.module?.path, name, "package.json") });
let tmp = { ...this.cfg?.srv?.module?.default, ...cfg };
tmp.options = { ...tmp.options, ...item.options };
obj = await this.helper.get(tmp);
}
// initialize plugins
if (obj) {
modules?.push(obj);
await this.initModuleSetup(obj, item);
this.emit('onLoadModule', [obj, name, _path.join(this.cfg.srv.module.path, name, "model"), this]);
}
return obj;
}
/**
* @description initialize the module options
* @returns {Object}
*/
initModuleOpts() {
return {};
}
/**
* @description initialize the module config
* @param {Object} module
* @param {Object} option
* @returns {Object} module
*/
initModuleSetup(module, option) {
return module;
}
}
module.exports = App;