nope/modules/mod-Broker/helpers/config-generator.ts
2020-09-10 18:21:19 +02:00

529 lines
15 KiB
TypeScript

/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2018-10-30 16:17:06
* @modify date 2020-09-09 15:58:44
* @desc [description]
*/
/** Clear the Screen */
declare const process: any;
import { exists, mkdir, readFile, writeFile } from "fs";
import { Container } from "inversify";
import { type } from "os";
/** Import Stuff for File-Handeling */
import { join, resolve } from "path";
import { promisify } from "util";
import { createPath, listFiles } from "../../../lib/helpers/fileMethods";
import { objectToMap, rgetattr } from "../../../lib/helpers/objectMethods";
import { IJsonSchema } from "../../../types/IJSONSchema";
import { DEFAULT_CONFIG_FILE, DEFAULT_SEARCHPATH } from "../../mod-Assembly-Builder/constants/constants";
import { Builder } from "../../mod-Assembly-Builder/src/Container-Builder.FileLoader";
import { SystemLogger } from "../../mod-Logger/src/Unique.Logger";
import { generateJSONSchemas } from "./converter.helper";
export const DEFAULT_STARTUP_CONFIG_FILE = "startup-configuration.json";
export const DEFAULT_PATH_TO_STARTUP_CONFIG_FILE =
"./config/startup-configuration.json";
const _logger = SystemLogger.logger.getLogger("module-loader");
export const FOLDER_SEPERATOR = type()
.toLowerCase()
.includes("windows")
? "\\"
: "/";
const _readFile = promisify(readFile);
const _writeFile = promisify(writeFile);
const _exists = promisify(exists);
const _mkdir = promisify(mkdir);
/**
* Function which will load multiple Modules
*
* @export
* @param {Container} container The Inversify-Container, which is used to build the Module
* @param {Array<{ path: string; typeDefinition?: string }>} config An Array containing the Element, which should be loaded
* @returns {Promise<des.MODULEBASE>} The loaded Module
*/
export async function asyncLoadModules(
container: Container,
config: Array<{ path: string; typeDefinition?: string }>,
dataUpdateCallback: () => void
): Promise<{ [index: string]: des.IMODULE }> {
/** Define the Return Type */
const _ret: { [index: string]: des.IMODULE } = {};
/** Scan for Modules and Add them */
for (const _element of config) {
/** Dynamically Load the Modules. */
const _file = _element.path;
try {
/** Import the Module */
const _mod = await import(resolve(_file));
_ret[_file] = await loadModule(_mod, container, _element.typeDefinition, dataUpdateCallback)
} catch (e) {
_logger.error(e, "could not import the Module:" + _file);
}
_logger.debug("loaded: " + _file);
}
_logger.info("All Modules have been loaded");
return _ret;
}
/**
* Function which will load an Extension
*
* @example // Load the Desired Module
* import * as AUTO_IBN_MODULE from '../modules/mod-Basic-Modules/src/auto-ibn-Module';
*
* const _module = loadModule(AUTO_IBN_MODULE, inversifyContainer, './schema/Auto-Ibn-Module.json');
*
* @export
* @param {{EXTENSION: any}} _mod The Module Object. (Normaly this defaults to the imported Module)
* @param {Container} container The Inversify-Container, which is used to build the Module
* @param {string} typeDefinition The Path, where Type-Definitions will be found
* @param {() => void} [dataUpdateCallback=() => {}] Callback for the Module on Updates.
* @returns {Promise<des.MODULEBASE>} The loaded Module
*/
export async function loadModule(_mod: { EXTENSION: any }, container: Container, typeDefinition: string, dataUpdateCallback: () => void = () => { }): Promise<des.MODULEBASE> {
if (_mod.EXTENSION) {
/** Load the Module */
const _extension: des.MODULEBASE = new _mod.EXTENSION(dataUpdateCallback);
/** Init the Module */
_extension.init(container);
/** Perform the onload-method */
_extension.onLoad();
/** If requried Load the Schema-JSON (only if it exsits) */
if (typeDefinition) {
if (await _exists(typeDefinition)) {
const _schema = JSON.parse(
await _readFile(typeDefinition, { encoding: "utf-8" })
);
for (const _funcName in _extension.functions) {
/** Adapt the Schema => */
const _func = _extension.functions[_funcName];
/** Extract the Schema for Inputs */
for (const t of ["input", "output"]) {
const _type = t as "input" | "output";
if (_func[_type].path) {
const _extractedSchema: IJsonSchema = rgetattr(
_schema,
_func[_type].path as string
);
if (_extractedSchema) {
const _schemaAsMap = objectToMap(_extractedSchema)
const _refs = Array.from(_schemaAsMap.entries()).filter(([key, value]) => key.endsWith('$ref')).map(([key, value]) => value as string);
if (_refs.length > 0) {
// Extract the Required Definitions
const _definitions = {};
_logger.warn('Requiring References for ' + _funcName, _refs);
console.log(_refs);
_func[_type].schema = _extractedSchema
} else {
// Assign the Extracted Schema to the Definition
_func[_type].schema = _extractedSchema
}
} else {
_logger.warn('Could not extract Schema for ' + _extension.name + '.' + _funcName + '. Using nulltype', _func[_type].path,);
_func[_type].schema = {
type: 'null'
}
}
}
}
}
}
}
return _extension;
}
}
/**
* Function to determine all Modules in the given Directory.
* After finding those, they will be loaded. Before this function
* can be called, make shure all assemblies are added to the
* Inversify-Container, otherwise this method will throw errors.
*
* @export
* @param {Container} : Promise<{ [index: string]: des.IMODULE }>
* @param {string} [path=DEFAULT_SEARCHPATH] A default Search Path is provided (./dist/modules)
* @returns {Promise<{ [index: string]: des.IMODULE }>} An object, using the Filename as Key an providing the loaded Module
*/
export async function asyncLoadAllModules(
container: Container,
dataUpdateCallback: () => void,
path: string = DEFAULT_SEARCHPATH
): Promise<{ [index: string]: des.IMODULE }> {
const _files = await listFiles(path, "-Module.js");
const _modules = new Array<{ path: string }>();
/** Adapt the Modules-Array to match the Required Shape */
for (const _file of _files) {
_modules.push({ path: _file });
}
return await asyncLoadModules(container, _modules, dataUpdateCallback);
}
/**
* Helper-Function which will load all Modules. But instead of running async, it
* perfroms in a Callback-Manner
*
* @export
* @param {Array<{ path: string }>} config
* @param {Container} container
* @param {(error: any, data: { [index: string]: des.IMODULE }) => void} callback
*/
export function loadModules(
config: Array<{ path: string }>,
container: Container,
dataUpdateCallback: () => void,
callback: (error: any, data: { [index: string]: des.IMODULE }) => void
): void {
/** Perform the Function with the given callbacks */
asyncLoadModules(container, config, dataUpdateCallback).then(
(result) => {
callback(undefined, result);
},
(error) => {
callback(error, {});
}
);
}
/**
* Helper Function, which will create a Default Module Config.
*
* @export
* @param {string} [_pathToFile=""]
* @param {boolean} [writeFile=true]
* @returns The Created Module-Description Files
*/
export async function writeDefaultModuleConfig(
_pathToFile: string = "",
writeFile = true
) {
const _extensions = await asyncLoadAllModules(
Builder.instance.container,
() => { },
DEFAULT_SEARCHPATH
);
/** Create the File Name */
const _dirName = join(process.cwd(), "config");
let _fileName = join(await createPath(_dirName), DEFAULT_CONFIG_FILE);
/** If a Path is given use this Path instead*/
if (_pathToFile) {
_fileName = _pathToFile;
}
/** Create a Config Element */
let _config = {};
/** Check if a Config exists => If So Extend the given config */
if (await _exists(_fileName)) {
_config = JSON.parse(await _readFile(_fileName, { encoding: "utf-8" }));
}
_config["modules"] = [];
/** Extract all files: */
const _files = await listFiles("./", "-Module-Types.ts");
/** Define the Schema-Dir */
const _schemaDir = join(resolve(process.cwd()), "schemas");
if (!(await _exists(_schemaDir))) {
await _mkdir(_schemaDir);
}
await generateJSONSchemas(
Object.getOwnPropertyNames(_extensions),
_files,
_schemaDir
);
/** Store the Config */
for (const _mod in _extensions) {
const _name = _mod.split(FOLDER_SEPERATOR)[
_mod.split(FOLDER_SEPERATOR).length - 1
];
let _typeName = _name.slice(0, _name.length - ".js".length) + "-Types.ts";
let _typeFile: string | undefined = undefined;
/** Search for the Type-Defintion File */
for (const _foundTypeFile of _files) {
if (_foundTypeFile.endsWith(_typeName)) {
_typeFile = _foundTypeFile;
break;
}
}
/** Adapt the Name */
_typeName = _typeName.split("-Module-Types.ts")[0];
/** Only if a type File is provided => Get the JSON-Schema */
if (!_typeFile) {
_logger.warn("No Module-Types found for " + _typeName);
for (const _func in _extensions[_mod].functions) {
const _funcDescriptor = _extensions[_mod].functions[_func];
const _schema = { definitions: {} };
if (
_funcDescriptor.input.schema !== undefined ||
_funcDescriptor.output.schema !== undefined
) {
Object.assign(_schema.definitions, _funcDescriptor.input.schema);
Object.assign(_schema.definitions, _funcDescriptor.output.schema);
await _writeFile(
join(_schemaDir, _typeName + ".json"),
JSON.stringify(_schema, undefined, 4)
);
}
}
}
_config["modules"].push({
name: _extensions[_mod].name,
description: _extensions[_mod].description,
path: _mod,
typeDefinition: join(_schemaDir, _typeName + ".json")
});
}
if (writeFile) {
/** Write the new Configuration */
await _writeFile(_fileName, JSON.stringify(_config, undefined, 4));
_logger.warn("Writing new Assembly-Config in :" + _fileName);
}
/** Return the created Filename */
return _config["modules"];
}
/**
* Function which will extract all Auto-Startup-Functions of a Module.
*
* @export
* @param {Container} container The Container containing all Assemblies
* @param {string} [_pathToFile=''] The Path to a File. Default = ./config/Startup-Config.json
* @returns
*/
export async function writeDefaultStartupConfig(
container: Container,
_pathToFile = DEFAULT_PATH_TO_STARTUP_CONFIG_FILE,
_assemblyFile = DEFAULT_CONFIG_FILE,
writeFile = true
) {
/** Load all Modules */
const _extensions = await asyncLoadModules(
container,
JSON.parse((await _readFile(_assemblyFile)).toString("utf-8")).modules,
() => { }
);
const _autostart = new Array<{ funcname: string; params: any }>();
/** Extract the autostart option on each Element */
for (const _name of Object.getOwnPropertyNames(_extensions)) {
const _extension = _extensions[_name];
_autostart.push(..._extension.autoStart);
}
/** Create the File Name */
const _dirName = join(resolve(process.cwd()), "config");
let _fileName = join(await createPath(_dirName), DEFAULT_STARTUP_CONFIG_FILE);
/** If a Path is given use this Path instead*/
if (_pathToFile) {
_fileName = _pathToFile;
}
if (writeFile) {
/** Write the Results */
await _writeFile(_fileName, JSON.stringify(_autostart, undefined, 4));
_logger.warn("Writing new Autostart-Config in :" + _fileName);
}
return _autostart;
}
async function writeUserConfig() {
process.stdout.write("\x1B[2J\x1B[0f");
// Import Inquirer and the Fuzzy-Path Module
const inquirer = require("inquirer");
inquirer.registerPrompt("path", require("inquirer-fuzzy-path"));
const nameInput = {
type: "input",
name: "fileName",
message: "Enter the Configuration File Name",
default: DEFAULT_CONFIG_FILE,
validate: function (value: string) {
if (value.endsWith(".json")) {
return true;
}
return "Please add the Enter a Valid Filename (including .json)";
}
};
let result = await inquirer.prompt([nameInput]);
/** Create the File Name */
const _dirName = join(process.cwd(), "config");
let _fileName = join(await createPath(_dirName), result.fileName);
/** List all Files which are used to describe an assembly */
const _mods = await writeDefaultModuleConfig(_fileName, false);
process.stdout.write("\x1B[2J\x1B[0f");
const files = {
type: "checkbox",
name: "elements",
message: "Select/Deselect the Assemblies",
choices: _mods.map((mod) => {
return {
name: mod.name,
checked: true
};
})
};
result = await inquirer.prompt([files]);
/** Create a Config Element */
let _config = {};
/** Check if a Config exists => If So Extend the given config */
if (await _exists(_fileName)) {
_config = JSON.parse(await _readFile(_fileName, { encoding: "utf-8" }));
}
_config["modules"] = _mods.filter((element) => {
if (result.elements.includes(element.name)) {
return element;
}
});
process.stdout.write("\x1B[2J\x1B[0f");
/** Write the new Configuration */
await _writeFile(_fileName, JSON.stringify(_config, undefined, 4));
_logger.warn("Writing new Assembly-Config in :" + _fileName);
process.stdout.write("\x1B[2J\x1B[0f");
result = await inquirer.prompt([
{
type: "confirm",
name: "writeStartup",
message: "Do you want to create a Startup-File?",
default: false
}
]);
if (result.writeStartup) {
result = await inquirer.prompt([
{
type: "input",
name: "fileName",
message: "Enter the File Name to store the auto-startup",
default: DEFAULT_STARTUP_CONFIG_FILE,
validate: function (value: string) {
if (value.endsWith(".json")) {
return true;
}
return "Please add the Enter a Valid Filename (including .json)";
}
}
]);
const _configFile = _fileName;
_fileName = join(await createPath(_dirName), result.fileName);
const _startup = await writeDefaultStartupConfig(
Builder.instance.container,
_fileName,
_configFile,
false
);
process.stdout.write("\x1B[2J\x1B[0f");
result = await inquirer.prompt([
{
type: "checkbox",
name: "elements",
message: "Select/Deselect the Assemblies",
choices: _startup.map((action) => {
return {
name: JSON.stringify(action),
short: action.funcname,
value: action.funcname,
checked: true
};
})
}
]);
/** Write the Results */
await _writeFile(
_fileName,
JSON.stringify(
_startup.filter((element) =>
result.elements.includes(element.funcname)
),
undefined,
4
)
);
_logger.warn("Writing new Autostart-Config in :" + _fileName);
}
}
if (typeof require != "undefined" && require.main == module) {
process.stdout.write("\x1B[2J\x1B[0f");
SystemLogger.logger.level = 'error';
SystemLogger.logger.lock('dontChange');
Builder.load();
Builder.on("loaded", () => {
writeUserConfig().catch(e => {
_logger.fatal(e, 'Failed creating config');
});
});
}