node-red-contrib-monogoto-customer
Version:
Allows communicating with the Monogoto Customer API from node-red.
375 lines (319 loc) • 15.9 kB
JavaScript
const path = require('path')
, SwaggerClient = require('swagger-client')
, yaml = require('yaml-js')
, fsPromise = require('fs-promise')
, CssEscape = require('css.escape')
, _ = require('lodash')
;
const nodeName = "monogoto-customer";
const swaggerSpecFile = "CustomerAPI.yaml";
const process = require('process');
const staticConfigParams = new Set(
[]
);
function SwaggerSpecManager() {
this._swaggerSpec = null;
this._info = null;
}
SwaggerSpecManager.prototype.getInfo = async function () {
if(this._info !== null) return this._info;
const choices = {};
const allParams = {};
const swaggerClient = await (new SwaggerClient({spec: await this.getSwaggerSpec()}));
for (const url of Object.keys(swaggerClient.spec.paths)) {
const methods = swaggerClient.spec.paths[url];
for (const httpMethodName of Object.keys(methods)) {
const operationDef = methods[httpMethodName];
if (!operationDef.operationId || operationDef === 'swagger_raw' || httpMethodName === 'x-swagger-router-controller') continue; // we only care about http methods such as GET POST PUT DELETE...
const operationId = operationDef.operationId;
const parameters = {};
if (operationDef.parameters) {
for (const paramInfo of operationDef.parameters) {
parameters[paramInfo.name] = {required: paramInfo.required, in: paramInfo.in, description:paramInfo.description, schema:paramInfo.schema};
if (!allParams[paramInfo.name]) allParams[paramInfo.name] = parameters[paramInfo.name];
}
}
const apiName = (operationDef.tags&&operationDef.tags.length)?
operationDef.tags[0] :
'default';
if (!choices[apiName]) choices[apiName] = {};
const operationDescription = {url, httpMethodName, parameters, description: operationDef.description};
if(operationDef.requestBody) {
operationDescription.requestBody = operationDef.requestBody;
}
choices[apiName][operationId] = operationDescription;
}
}
this._info = {choices: choices, allParams: allParams};
return this._info;
};
SwaggerSpecManager.prototype.getSwaggerSpec = async function () {
if(this._swaggerSpec !== null) return this._swaggerSpec;
const swaggerSpecPath = path.resolve(__dirname, 'static', swaggerSpecFile);
this._swaggerSpec =
swaggerSpecFile.toLowerCase().endsWith('.json')?
require(swaggerSpecPath) :
yaml.load(await fsPromise.readFile(swaggerSpecPath, 'UTF-8'));
return this._swaggerSpec;
};
const swaggerSpecManager = new SwaggerSpecManager();
module.exports = async function(RED) {
const debuglength = RED.settings.debugMaxLength || 1000;
function sendDebug(msg) {
msg = RED.util.encodeObject(msg,{maxLength:debuglength});
RED.comms.publish("debug",msg);
}
function evaluateNodePropertyPromise(value, type, node, msg) {
return new Promise(function(resolve, reject) {
RED.util.evaluateNodeProperty(value, type, node, msg, function(err, val) {
err? reject(err):resolve(val);
})
})
}
function SwaggerApiNode(config) { RED.nodes.createNode(this, config); (async ()=>{
const node = this;
// Retrieve the config node
this.server = RED.nodes.getNode(config.server);
let requestsCounter = 0;
function updatePendingRequests(inc) {
requestsCounter += inc? 1:-1;
if(requestsCounter <= 0) node.status({});
else {
node.status({fill: "blue", shape: "dot", text: "Pending Requests: " + requestsCounter});
}
}
function getLoggingCfg() {
let cfg = {};
if(node.server?.logging) {
Object.assign(cfg, JSON.parse(JSON.stringify(node.server.logging)));
}
try {
const envVarName = node.server.loggingCfgFromEnvVar.trim();
const envVarLoginCfg = JSON.parse(process.env[envVarName]);
if(envVarLoginCfg.request) {
if(!cfg.request) cfg.request = {};
Object.assign(cfg.request, envVarLoginCfg.request)
}
if(envVarLoginCfg.response) {
if(!cfg.response) cfg.response = {};
Object.assign(cfg.response, envVarLoginCfg.response)
}
} catch(e) {
// Env var does not contain a valid json, so using only from config node value
}
return cfg;
}
let swaggerClient;
let lastReq;
function baseRequestInterceptor(req, msg) {
lastReq = req;
if(node.server && node.server.token) {
req.headers['Authorization'] = 'Bearer ' + node.server.token;
}
if(node.server && node.server.cookie) {
req.headers['Cookie'] = node.server.cookie;
}
const logReq = Object.assign({}, req);
if(logReq.body && typeof logReq.body === 'string') {
try {
logReq.body = JSON.parse(logReq.body);
} catch(e) {}
}
if(node.hasOwnProperty('customerId')) {
req.headers['apikey'] = node.customerId;
}
const logging = getLoggingCfg();
if(logging?.request) {
let logStr = `Request (${logReq.method}) from ${node.id} ${config['node-label']||''}`;
const logObj = {method: logReq.method};
if(logging?.request?.url) logObj.url = logReq.url
if(logging?.request?.headers) logObj.headers = logReq.headers;
if(logging?.request?.body) logObj.body = logReq.body;
if(Object.keys(logObj).length) {
logStr += ` ${JSON.stringify(logObj, null, 2)}`;
}
node.log(logStr);
sendDebug({id:node.id, z:node.z, _alias: node._alias, path:node._flow.path, name:node.name, topic:"Request",
msg: msg? { _msgid: msg._msgid, ...logObj} : undefined
});
}
return req;
}
function baseResponseInterceptor(res, msg) {
const clonedRes = _.cloneDeep(res);
if(clonedRes.body) {
delete clonedRes.data;
}
delete clonedRes.text;
delete clonedRes.obj;
const logging = getLoggingCfg();
if(logging?.response) {
let logStr = `Response of ${node.id} ${config['node-label']||''}`
const logObj = {};
if(logging?.response?.headers) logObj.headers = clonedRes.headers;
if(logging?.response?.body) logObj.body = clonedRes.body;
if(Object.keys(logObj).length) {
logStr += ` ${JSON.stringify(logObj, null, 2)}`;
}
node.log(logStr);
sendDebug({id:node.id, z:node.z, _alias: node._alias, path:node._flow.path, name:node.name, topic:"Response",
msg: msg? { _msgid: msg._msgid, ...logObj} : undefined
});
}
return res;
}
try {
swaggerClient = await SwaggerClient({
spec: await swaggerSpecManager.getSwaggerSpec(),
});
if(this.server && this.server.host) {
const server = !this.server["host-type"]? this.server.host:
await evaluateNodePropertyPromise(this.server.host, this.server["host-type"], this.server, {});
if(swaggerClient.spec.servers) {
swaggerClient.spec.servers = [{url:server}];
} else {
swaggerClient.spec.host = server;
}
}
} catch (reason) {
node.error("failed to load swagger spec file.\r\n" + reason);
}
node.on('input', (async (msg)=>{
let request = null;
function sendAndSaveRequest(swaggerClientOperation, resolvedParams, extraParams) {
const argsToSend = Array.from(arguments).slice(1);
const promise = swaggerClientOperation.apply(swaggerClientOperation, argsToSend);
request = lastReq;
return promise;
}
updatePendingRequests(true);
const sendReq = async (alreadyTriedLogin)=>{
try {
const operationInfo = (await swaggerSpecManager.getInfo()).choices[config.api][config.operation];
const resolvedParams = {};
const operationParamNames = new Set(Object.keys(operationInfo.parameters));
for (const paramName of operationParamNames) {
const paramValueInField = config['param-' + CssEscape(paramName)];
const paramTypedInput = config['param-' + CssEscape(paramName) + '-type'];
const hasDynamicValue = msg.params && (typeof msg.params[paramName] !== "undefined");
resolvedParams[paramName] = hasDynamicValue ? msg.params[paramName] : await evaluateNodePropertyPromise(paramValueInField, paramTypedInput, node, msg);
}
const paramsToSetFromConfig = new Set(Array.from(staticConfigParams).filter(x => operationParamNames.has(x)));
for(const paramName of paramsToSetFromConfig) {
resolvedParams[paramName] = this.server[paramName];
}
if(config.isAdmin) {
try {
node.customerId = await evaluateNodePropertyPromise(config.customerId, config['customerId-type'], node, msg);
} catch (e) {}
} else {
delete node.customerId;
}
const extraParams = {};
if(operationInfo.requestBody) {
extraParams.requestBody = await evaluateNodePropertyPromise(config.requestBody, config['requestBody-type'], node, msg);
}
const swaggerClientOperation = swaggerClient.apis[config.api][config.operation];
extraParams.requestInterceptor = function(req) {
req = baseRequestInterceptor(req, msg);
if(msg?.headers && Object.keys(msg.headers).length) {
if(!req.headers || !Object.keys(req.headers).length) req.headers = {};
Object.assign(req.headers, msg.headers);
}
return req;
}
extraParams.responseInterceptor = function(req) {
req = baseResponseInterceptor(req, msg);
return req;
}
const swaggerResponse = await sendAndSaveRequest(swaggerClientOperation, resolvedParams, extraParams);
if(swaggerResponse.hasOwnProperty('body') &&
(
swaggerResponse.body.constructor === {}.constructor ||
swaggerResponse.body.constructor === [].constructor
)
) {
msg.payload = swaggerResponse.body;
msg.responseHeaders=swaggerResponse.headers;
} else {
msg.payload = swaggerResponse.data;
msg.responseHeaders=swaggerResponse.headers;
}
updatePendingRequests(false);
node.send(msg);
} catch (error) {
try {
if (error.statusCode === 401 && !alreadyTriedLogin && this.server) {
const uname = await evaluateNodePropertyPromise(this.server.username, this.server['username-type'], node, msg);
const pass = await evaluateNodePropertyPromise(this.server.password, this.server['password-type'], node, msg)
const response = await sendAndSaveRequest(swaggerClient.apis.Auth.auth_LoginWebApi, {
body: {
UserName: uname,
Password: pass
}
});
if(this.server) {
this.server.token = response.body.token;
this.server.cookie = response.headers['set-cookie'];
}
setImmediate(sendReq.bind(this), true);
} else {
throw error;
}
} catch (error) {
msg.payload = {};
if(error.response) {
msg.payload.response = _.cloneDeep(error.response);
// Cleanup
delete msg.payload.statusCode;
if(msg.payload.hasOwnProperty('response')) {
delete msg.payload.response.statusCode;
delete msg.payload.response.statusText;
delete msg.payload.response.text;
delete msg.payload.response.obj;
if(msg.payload.response.body) {
delete msg.payload.response.data;
}
}
if(request) {
delete request.requestInterceptor;
delete request.responseInterceptor;
if(request.body && typeof request.body === 'string') {
try {
request.body = JSON.parse(request.body);
} catch (e) {}
}
msg.payload.request = request;
}
} else {
msg.payload.error = error;
}
updatePendingRequests(false);
if(!config.outErrors) {
node.error(nodeName, msg);
} else {
node.send([undefined,msg]);
}
}
}
};
setImmediate(sendReq.bind(this), false);
}));
})();}
RED.httpAdmin.get('/' + nodeName + '/ui-choices', async function (req, res) {
res.send(await swaggerSpecManager.getInfo());
});
RED.nodes.registerType(nodeName, SwaggerApiNode);
function SwaggerApiNodeServer(n) {
RED.nodes.createNode(this, n);
this.host = n.host;
this['host-type'] = n['host-type'];
this.password = n.password;
this['password-type'] = n['password-type'];
this.username = n.username;
this['username-type'] = n['username-type']
this.logging = n.logging;
this.loggingCfgFromEnvVar = n.loggingCfgFromEnvVar;
}
RED.nodes.registerType(nodeName + "-server", SwaggerApiNodeServer);
};