UNPKG

node-red-contrib-monogoto-customer

Version:

Allows communicating with the Monogoto Customer API from node-red.

375 lines (319 loc) 15.9 kB
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); };