@runnerty/module-core
Version:
Runnerty module core classes
493 lines (450 loc) • 16.5 kB
JavaScript
'use strict';
const ms = require('millisecond');
const lodash = require('lodash');
const stringify = require('json-stringify-safe');
const interpreter = require('@runnerty/interpreter-core');
class Executor {
constructor(args) {
this.logger = args.logger;
this.checkExecutorParams = args.checkExecutorParams;
const params = Object.keys(args.process.exec);
let paramsLength = params.length;
while (paramsLength--) {
if (params[paramsLength] === 'type') {
this.logger.log(
'error',
`Params of "${args.process.id}" contains no allowed "type" parameter, will be ignored.`
);
} else {
this[params[paramsLength]] = args.process.exec[params[paramsLength]];
}
}
this.process = args.process;
this.processId = args.process.id;
this.processName = args.process.name;
this.processUId = args.process.uId;
this.runtime = args.runtime;
}
async init() {
try {
const configValues = await this.process.loadExecutorConfig();
if (!this.type && configValues.type) {
this.type = configValues.type;
}
this.config = configValues;
const execToCheck = Object.assign({}, this.process.exec);
execToCheck.config = configValues;
execToCheck.type = this.type;
this.checkExecutorParams(execToCheck);
return this;
} catch (err) {
this.logger.log('error', `init Executor:`, err);
throw err;
}
}
async execMain(process_resolve, process_reject) {
this.resolve = process_resolve;
this.reject = process_reject;
try {
const values = await this.getValues();
this.exec(values);
} catch (err) {
this.logger.log('error', `execMain Executor:`, err);
this.process.execute_err_return = `execMain Executor: ${err}`;
this.process.msg_output = '';
await this.process.error();
this.reject(`execMain Executor: ${err}`);
}
}
async exec() {
this.logger.log('error', 'Method exec (execution) must be rewrite in child class');
this.process.execute_err_return = 'Method exec (execution) must be rewrite in child class';
this.process.msg_output = '';
await this.process.error();
throw new Error('Method exec (execution) must be rewrite in child class');
}
async killMain(reason, options) {
try {
const values = await this.getValues();
this.kill(values, reason, options);
} catch (err) {
this.logger.log('error', `killMain Executor:`, err);
this.process.execute_err_return = `killMain Execution ${err}`;
this.process.msg_output = '';
await this.process.error();
throw new Error(`killMain Execution ${err}`);
}
}
kill(params, reason, options) {
this.logger.log('warn', this.processId, 'killed: ', reason);
this.process.execute_err_return = this.processId + ' - killed: ' + reason;
this.process.msg_output = '';
this.end(options);
}
async end(options) {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (!options) {
options = {};
}
options.end = options.end || 'end';
this.process.execute_arg = options.execute_arg;
this.process.command_executed = options.command_executed;
let outputParsed;
//OUTPUT FILTER (data_output):
if (this.process.output_filter && options.data_output) {
try {
if (options.data_output instanceof Object) {
outputParsed = options.data_output;
} else {
outputParsed = JSON.parse(options.data_output);
}
//OVERRIDE DATA OUTPUT:
options.data_output = await this.applyOuputFilter(this.process.output_filter, outputParsed);
outputParsed = options.data_output;
} catch (err) {
this.logger.log('warn', `The output of process ${this.process.id}, is not a filterable object.`);
}
}
//OUTPUT ORDER (data_output):
if (this.process.output_order && options.data_output) {
try {
if (!outputParsed) {
if (options.data_output instanceof Object) {
outputParsed = options.data_output;
} else {
outputParsed = JSON.parse(options.data_output);
}
}
//OVERRIDE DATA OUTPUT:
options.data_output = this.applyOutputOrder(this.process.output_order, outputParsed);
} catch (err) {
this.logger.log('warn', `The output of process ${this.process.id}, is not a sortable object.`);
}
}
let extraOutputParsed;
//OUTPUT FILTER (extra_output):
if (this.process.output_filter && options.extra_output) {
try {
if (options.extra_output instanceof Object) {
extraOutputParsed = options.extra_output;
} else {
extraOutputParsed = JSON.parse(options.extra_output);
}
//OVERRIDE DATA OUTPUT:
options.extra_output = await this.applyOuputFilter(this.process.output_filter, extraOutputParsed);
extraOutputParsed = options.extra_output;
} catch (err) {
this.logger.log('warn', `The extra_output of process ${this.process.id}, is not a filterable object.`);
}
}
//OUTPUT ORDER (extra_output):
if (this.process.output_order && options.extra_output) {
try {
if (!extraOutputParsed) {
if (options.data_output instanceof Object) {
extraOutputParsed = options.extra_output;
} else {
extraOutputParsed = JSON.parse(options.extra_output);
}
}
//OVERRIDE DATA OUTPUT:
options.extra_output = this.applyOutputOrder(this.process.output_order, extraOutputParsed);
} catch (err) {
this.logger.log('warn', `The extra_output of process ${this.process.id}, is not a sortable object.`);
}
}
//STANDARD OUPUT:
this.process.data_output =
options.data_output instanceof Object ? stringify(options.data_output) : options.data_output || '';
this.process.msg_output = options.msg_output || '';
//EXTRA DATA OUTPUT:
if (options.extra_output) {
this.process.extra_output = this.JSON2KV(options.extra_output, '_', 'PROCESS_EXEC');
}
switch (options.end) {
case 'error':
if (this.process.retries && !this.process.retries_count) this.process.retries_count = 0;
// RETRIES:
if (this.process.retries && this.process.retries_count < this.process.retries) {
const willRetry = true;
const writeOutput = true;
this.process.err_output = options.err_output;
// NOTIFICATE ONLY LAST FAIL: notificate_only_last_fail
await this.process.error(!this.process.notificate_only_last_fail, writeOutput, willRetry);
// RETRIES DELAY:
setTimeout(() => {
this.process.retries_count = (this.process.retries_count || 0) + 1;
this.process.err_output = '';
this.process.retry();
this.execMain(this.resolve, this.reject);
}, ms(this.process.retry_delay));
} else {
this.process.err_output = options.err_output;
await this.process.error();
this.reject(options.messageLog || '');
}
break;
default:
await this.process.end();
this.resolve();
break;
}
}
async paramsReplace(input, options) {
const useGlobalValues = options.useGlobalValues || true;
const useProcessValues = options.useProcessValues || false;
const useExtraValue = options.useExtraValue || false;
const _options = {
ignoreGlobalValues: !useGlobalValues
};
if (options.altValueReplace) {
_options.altValueReplace = options.altValueReplace;
}
const replacerValues = {};
//Process values
if (useProcessValues) {
Object.assign(replacerValues, this.process.values());
}
// Custom object values:
if (useExtraValue) {
Object.assign(replacerValues, useExtraValue);
}
try {
const replacedValues = await interpreter(
input,
replacerValues,
_options,
this.runtime.config?.interpreter_max_size,
this.runtime.config?.global_values
);
return replacedValues;
} catch (err) {
this.logger.log('error', 'Execution - Method getValues:', err);
this.process.err_output = 'Execution - Method getValues:' + err;
this.process.msg_output = '';
await this.process.error();
throw err;
}
}
// Return config and params values:
async getValues() {
try {
const configValues = await this.process.loadExecutorConfig();
const values = {};
Object.assign(values, configValues);
Object.assign(values, this.process.exec);
if (this.process.exec.type && configValues.type) {
values.type = configValues.type;
}
const repacedValues = await interpreter(
values,
this.process.values(),
undefined,
this.runtime.config?.interpreter_max_size,
this.runtime.config?.global_values
);
return repacedValues;
} catch (err) {
this.logger.log('error', 'Execution - Method getValues / loadExecutorConfig:', err);
this.process.err_output = 'Execution - Method getValues / loadExecutorConfig:' + err;
this.process.msg_output = '';
await this.process.error();
throw err;
}
}
async getParamValues() {
try {
const res = await interpreter(
this.process.exec,
this.process.values(),
undefined,
this.runtime.config?.interpreter_max_size,
this.runtime.config?.global_values
);
return res;
} catch (err) {
this.logger.log('error', 'Execution - Method getParamValues:', err);
this.process.err_output = 'Execution - Method getParamValues:' + err;
this.process.msg_output = '';
await this.process.error();
throw err;
}
}
async getConfigValues() {
try {
const configValues = await this.chain.loadExecutorConfig();
const replacedValues = await interpreter(
configValues,
this.process.values(),
undefined,
this.runtime.config?.interpreter_max_size,
this.runtime.config?.global_values
);
return replacedValues;
} catch (err) {
this.logger.log('error', 'Execution - Method getConfigValues:', err);
this.process.err_output = 'Execution - Method getConfigValues:' + err;
this.process.msg_output = '';
await this.process.error();
throw err;
}
}
JSON2KV(objectToPlain, separator, prefix) {
const _self = this;
const res = {};
// Sub function: Recursive call to flatten objects:
function _iterateObject(key, object2KV) {
// Si el objeto no está vacio:
if (Object.keys(object2KV).length) {
res[key] = object2KV;
// Recursive call to obtain key / value of the entire object tree:
const sub_res = _self.JSON2KV(object2KV, separator);
const sub_res_keys = Object.keys(sub_res);
// Loop through the result to include in "res" all keys / value including current key:
for (let i = 0; i < sub_res_keys.length; i++) {
res[key + separator + sub_res_keys[i]] = sub_res[sub_res_keys[i]];
}
} else {
// If the object is empty we return the current key with null value:
res[key] = null;
}
}
const eobjs = Object.keys(objectToPlain);
// We iterate through the object to flatten:
for (let i = 0; i < eobjs.length; i++) {
// Generate the key from the key of the iteration item. In case of arriving prefix it is included and we always do uppercase:
const key = prefix ? prefix + separator + eobjs[i].toUpperCase() : eobjs[i].toUpperCase();
// Check if it is an object:
if (
objectToPlain[eobjs[i]] &&
typeof objectToPlain[eobjs[i]] === 'object' &&
objectToPlain[eobjs[i]].constructor === Object
) {
// Call to the sub-function:
_iterateObject(key, objectToPlain[eobjs[i]]);
} else {
// If instead of an object it is an array, as many key / value will be created as there are items in the array, including the position of the value in the key:
if (Array.isArray(objectToPlain[eobjs[i]])) {
const arrValues = objectToPlain[eobjs[i]];
const arrLength = arrValues.length;
// Add the parent array and not only its elements to the response:
res[key] = arrValues;
for (let z = 0; z < arrLength; z++) {
// In case the array has objects:
if (arrValues[z] && typeof arrValues[z] === 'object' && arrValues[z].constructor === Object) {
// Call to the sub-function, including the position of the array in the key:
_iterateObject(key + separator + z, arrValues[z]);
} else {
// If it is not an object, we include in res the value in the key with the position of the array:
res[key + separator + z] = arrValues[z];
}
}
} else {
// If it is neither object nor array, we return the value with the key:
res[key] = objectToPlain[eobjs[i]];
}
}
}
// Returns the accumulated values in res of the entire object tree:
return res;
}
applyOutputOrder(orderBy, output) {
if (orderBy.length) {
const orderFields = [];
const orderFieldsOrder = [];
orderBy.forEach(orderField => {
const [field, order] = orderField.split(' ');
orderFields.push(field);
orderFieldsOrder.push(order || 'asc');
});
return lodash.orderBy(output, orderFields, orderFieldsOrder);
} else {
return output;
}
}
async applyOuputFilter(filter, output) {
if (filter instanceof Object) {
const operator = Object.keys(filter)[0];
let res;
if (filter[operator] instanceof Array) {
switch (operator) {
case '$or':
res = [];
for (const conditionFilter of filter[operator]) {
const resFilter = await this.applyOuputFilter(conditionFilter, output);
res = lodash.uniqWith([...res, ...resFilter], lodash.isEqual);
}
break;
case '$and':
res = output;
for (const conditionFilter of filter[operator]) {
res = await this.applyOuputFilter(conditionFilter, res);
}
break;
default:
break;
}
} else {
res = await this.applyConditionOuputFilter(filter, output);
}
return res;
} else {
throw new Error('Check output_filter, is not valid object.');
}
}
regExpFromString(q) {
let flags = q.replace(/.*\/([gimuy]*)$/, '$1');
if (flags === q) flags = '';
const pattern = flags ? q.replace(new RegExp('^/(.*?)/' + flags + '$'), '$1') : q;
try {
return new RegExp(pattern, flags);
} catch (e) {
return null;
}
}
async applyConditionOuputFilter(condition, output) {
const item = Object.keys(condition)[0];
let valueOriginal = '';
let operator = '';
if (condition[item] instanceof Object) {
operator = Object.keys(condition[item])[0];
valueOriginal = condition[item][operator];
} else {
// Condition not set, equal to {"$eq": VALUE}
operator = '$eq';
valueOriginal = condition[item];
}
const value = await interpreter(
valueOriginal,
undefined,
undefined,
this.runtime.config?.interpreter_max_size,
this.runtime.config?.global_values
);
switch (operator) {
case '$eq':
return output.filter(i => i[item] == value);
case '$match':
return output.filter(i => typeof i[item] === 'string' && i[item].match(this.regExpFromString(value)));
case '$lt':
return output.filter(i => i[item] < value);
case '$lte':
return output.filter(i => i[item] <= value);
case '$gt':
return output.filter(i => i[item] > value);
case '$gte':
return output.filter(i => i[item] >= value);
case '$ne':
return output.filter(i => i[item] != value);
case '$in':
return output.filter(i => value.includes(i[item]));
case '$nin':
return output.filter(i => !value.includes(i[item]));
default:
return [];
}
}
}
module.exports = Executor;