UNPKG

node-red-contrib-mssql-plus

Version:

A node-red node to execute queries, stored procedures and bulk inserts in Microsoft SQL Server and Azure Databases SQL2000 ~ SQL2022

842 lines (790 loc) 37.2 kB
/* eslint-disable spaced-comment */ /* eslint-disable space-before-function-paren */ module.exports = function (RED) { 'use strict'; const mustache = require('mustache'); const sql = require('mssql'); const UUID = (function () { const crypto = require('crypto'); // isValid routine developed based on \tedious\lib\guid-parser.js // checking the guid in this node BEFORE calling pool.query() // prevents an exception that I cannot catch in tedious const CHARCODEMAP = {}; const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'].map(d => d.charCodeAt(0)); for (let i = 0; i < hexDigits.length; i++) { const map = CHARCODEMAP[hexDigits[i]] = {}; for (let j = 0; j < hexDigits.length; j++) { const hex = String.fromCharCode(hexDigits[i], hexDigits[j]); const value = parseInt(hex, 16); map[hexDigits[j]] = value; } } return { /** * Tests a string to ensure it is a valid RFC4122 UUID. * @param {string} guid * @returns {boolean} true if valid */ isValid: function (guid) { try { return Array.isArray([CHARCODEMAP[guid.charCodeAt(6)][guid.charCodeAt(7)], CHARCODEMAP[guid.charCodeAt(4)][guid.charCodeAt(5)], CHARCODEMAP[guid.charCodeAt(2)][guid.charCodeAt(3)], CHARCODEMAP[guid.charCodeAt(0)][guid.charCodeAt(1)], CHARCODEMAP[guid.charCodeAt(11)][guid.charCodeAt(12)], CHARCODEMAP[guid.charCodeAt(9)][guid.charCodeAt(10)], CHARCODEMAP[guid.charCodeAt(16)][guid.charCodeAt(17)], CHARCODEMAP[guid.charCodeAt(14)][guid.charCodeAt(15)], CHARCODEMAP[guid.charCodeAt(19)][guid.charCodeAt(20)], CHARCODEMAP[guid.charCodeAt(21)][guid.charCodeAt(22)], CHARCODEMAP[guid.charCodeAt(24)][guid.charCodeAt(25)], CHARCODEMAP[guid.charCodeAt(26)][guid.charCodeAt(27)], CHARCODEMAP[guid.charCodeAt(28)][guid.charCodeAt(29)], CHARCODEMAP[guid.charCodeAt(30)][guid.charCodeAt(31)], CHARCODEMAP[guid.charCodeAt(32)][guid.charCodeAt(33)], CHARCODEMAP[guid.charCodeAt(34)][guid.charCodeAt(35)]]); } catch (error) { return false; } }, /** * Generates a V4 RFC4122 UUID. * @returns {string} UUID */ v4: function () { const buf = crypto.randomBytes(32); let idx = 0; return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = buf[idx++] & 0xf; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } }; })(); /** * extractTokens - borrowed from @0node-red/nodes/core/core/80-template.js */ function extractTokens(tokens, set) { set = set || new Set(); tokens.forEach(function (token) { if (token[0] !== 'text') { set.add(token[1]); if (token.length > 4) { extractTokens(token[4], set); } } }); return set; } const sqlParamTypes = { 'varchar': { params: [], sqlType: 'VarChar' }, 'varchar(?)': { params: ['nm'], sqlType: 'VarChar' }, 'nvarchar': { params: [], sqlType: 'NVarChar' }, 'nvarchar(?)': { params: ['nm'], sqlType: 'NVarChar' }, 'text': { params: [], sqlType: 'Text' }, 'int': { params: [], sqlType: 'Int' }, 'bigint': { params: [], sqlType: 'BigInt' }, 'tinyint': { params: [], sqlType: 'TinyInt' }, 'smallint': { params: [], sqlType: 'SmallInt' }, 'bit': { params: [], sqlType: 'Bit' }, 'float': { params: [], sqlType: 'Float' }, 'numeric': { params: [], sqlType: 'Numeric' }, 'numeric(?,?)': { params: ['n', 'n'], sqlType: 'Numeric' }, 'decimal': { params: [], sqlType: 'Decimal' }, 'decimal(?,?)': { params: ['n', 'n'], sqlType: 'Decimal' }, 'real': { params: [], sqlType: 'Real' }, 'date': { params: [], sqlType: 'Date' }, 'datetime': { params: [], sqlType: 'DateTime' }, 'datetime2': { params: [], sqlType: 'DateTime2' }, 'datetime2(?)': { params: ['n'], sqlType: 'DateTime2' }, 'datetimeoffset': { params: [], sqlType: 'DateTimeOffset' }, 'datetimeoffset(?)': { params: ['n'], sqlType: 'DateTimeOffset' }, 'smalldatetime': { params: [], sqlType: 'SmallDateTime' }, 'time': { params: [], sqlType: 'Time' }, 'time(?)': { params: ['n'], sqlType: 'Time' }, 'uniqueidentifier': { params: [], sqlType: 'UniqueIdentifier' }, 'smallmoney': { params: [], sqlType: 'SmallMoney' }, 'money': { params: [], sqlType: 'Money' }, 'binary': { params: [], sqlType: 'Binary' }, 'varbinary': { params: [], sqlType: 'VarBinary' }, 'varbinary(?)': { params: ['nm'], sqlType: 'VarBinary' }, 'image': { params: [], sqlType: 'Image' }, 'xml': { params: [], sqlType: 'Xml' }, 'char': { params: [], sqlType: 'Char' }, 'char(?)': { params: ['n'], sqlType: 'Char' }, 'nchar': { params: [], sqlType: 'NChar' }, 'nchar(?)': { params: ['n'], sqlType: 'NChar' }, 'ntext': { params: [], sqlType: 'NText' }, 'tvp': { params: [], sqlType: 'TVP' }, 'tvp(?)': { params: ['p'], sqlType: 'TVP' }, 'udt': { params: [], sqlType: 'UDT' }, 'geography': { params: [], sqlType: 'Geography' }, 'geometry': { params: [], sqlType: 'Geometry' }, 'variant': { params: [], sqlType: 'Variant' } }; function coerceType(sqlType) { if (!sqlType) return null; //no type specified (infered) const st = sqlType.trim(); const sl = st.toLowerCase(); const bp = sl.indexOf('('); let typeName; let typeParams = ''; let p; let p2; if (bp > -1) { //has bracket? typeName = sl.slice(0, bp).trim();//get typename without params typeParams = '(?)'; //additional typename param for sqlParamTypes lookup const _params = st.slice(bp + 1, -1);//get params const hasComma = _params.indexOf(',') > 0; //more than one param? if (hasComma && (sl.includes('decimal') || sl.includes('numeric'))) { typeParams = '(?,?)'; //additional typename param for sqlParamTypes lookup const params = _params.split(','); p = params[0]; p2 = params[1]; } else { p = _params; } } else { typeName = sl; //use lowercase type name for sqlParamTypes lookup } //lookup the desired type from sqlParamTypes const _type = sqlParamTypes[typeName + typeParams]; if (_type) { const params = []; if (_type.params.length >= 1) { if (_type.params[0] === 'nm' && (p + '').toLowerCase() === 'max') { params.push('MAX'); } else if (_type.params[0] === 'n') { params.push(parseInt(p)); } else { params.push(p); } } if (_type.params.length >= 2) { if (_type.params[1] === 'nm' && (p2 + '').toLowerCase() === 'max') { params.push('MAX'); } else if (_type.params[1] === 'n') { params.push(parseInt(p2)); } else if (_type.params[0] === 'n') { params.push(p2); } } if (params.length) { return sql[_type.sqlType](...params);//e.g. sql.NChar(10) } return sql[_type.sqlType]();//e.g. sql.NChar() } throw new Error(`Unable to determine the type or its properties from '${sqlType}'`); } /** * parseContext - borrowed from @0node-red/nodes/core/core/80-template.js */ function parseContext(key) { const match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key); if (match) { const parts = {}; parts.type = match[1]; parts.store = (match[3] === '') ? 'default' : match[3]; parts.field = match[4]; return parts; } return undefined; } /** * parseEnv - borrowed from @0node-red/nodes/core/core/80-template.js */ function parseEnv(key) { const match = /^env\.(.+)/.exec(key); if (match) { return match[1]; } return undefined; } /** * checks if `n` is a valid number. * @param {string | number} n */ function isNumber (n) { return !isNaN(parseFloat(n)) && isFinite(n); } /** * Parse a string or number into an integer and return it. * If `n` cannot be parsed, `defaultValue` is returned instead. * @param {string | number} n * @param {boolean} defaultValue - the value to return if `n` is not a valid number * @returns {integer} `n` parsed to an integer or `defaultValue` if `n` is invalid */ function safeParseInt (n, defaultValue) { try { const x = parseInt(n); if (isNumber(x)) { return x; } } catch (error) { // do nothing } return defaultValue; } /** * NodeContext - borrowed from @0node-red/nodes/core/core/80-template.js */ function NodeContext (msg, nodeContext, parent, escapeStrings, cachedContextTokens) { this.msgContext = new mustache.Context(msg, parent); this.nodeContext = nodeContext; this.escapeStrings = escapeStrings; this.cachedContextTokens = cachedContextTokens; } NodeContext.prototype = new mustache.Context(); NodeContext.prototype.lookup = function (name) { let value = this.msgContext.lookup(name); if (value !== undefined) { if (this.escapeStrings && typeof value === 'string') { value = value.replace(/\\/g, '\\\\'); value = value.replace(/\n/g, '\\n'); value = value.replace(/\t/g, '\\t'); value = value.replace(/\r/g, '\\r'); value = value.replace(/\f/g, '\\f'); value = value.replace(/[\b]/g, '\\b'); } return value; } // try env if (parseEnv(name)) { return this.cachedContextTokens[name]; } // try flow/global context: const context = parseContext(name); if (context) { const type = context.type; // const store = context.store; // const field = context.field; const target = this.nodeContext[type]; if (target) { return this.cachedContextTokens[name]; } } return ''; }; NodeContext.prototype.push = function push(view) { return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens); }; function connection(config) { RED.nodes.createNode(this, config); const node = this; //add mustache transformation to connection object const configStr = JSON.stringify(config); const transform1 = mustache.render(configStr, process.env); config = JSON.parse(transform1); //add mustache transformation to credentials object try { const credStr = JSON.stringify(node.credentials); const transform2 = mustache.render(credStr, process.env); node.credentials = JSON.parse(transform2); } catch (error) { console.error(error); } node.config = { user: (node.credentials ? node.credentials.username : '') || '', password: (node.credentials ? node.credentials.password : '') || '', domain: node.credentials ? node.credentials.domain : '', server: config.server, database: config.database, options: { port: config.port ? safeParseInt(config.port, 1433) : undefined, tdsVersion: config.tdsVersion || '7_4', encrypt: config.encyption, trustServerCertificate: !((config.trustServerCertificate === 'false' || config.trustServerCertificate === false)), //defaults to true for backwards compatibility - TODO: Review this default. useUTC: config.useUTC, connectTimeout: config.connectTimeout ? safeParseInt(config.connectTimeout, 15000) : undefined, requestTimeout: config.requestTimeout ? safeParseInt(config.requestTimeout, 15000) : undefined, cancelTimeout: config.cancelTimeout ? safeParseInt(config.cancelTimeout, 5000) : undefined, camelCaseColumns: (config.camelCaseColumns === 'true' || config.camelCaseColumns === true) ? true : undefined, //defaults to undefined. parseJSON: !!((config.parseJSON === 'true' || config.parseJSON === true)), //defaults to true. enableArithAbort: !((config.enableArithAbort === 'false' || config.enableArithAbort === false)), //defaults to true. readOnlyIntent: (config.readOnlyIntent === 'true' || config.readOnlyIntent === true) //defaults to false. }, pool: { max: safeParseInt(config.pool, 5), min: 0, idleTimeoutMillis: 3000 //log: (message, logLevel) => console.log(`POOL: [${logLevel}] ${message}`) } }; //config options seem to differ between pool and tedious connection //so for compatibility I just repeat the ones that differ so they get picked up in _poolCreate () node.config.port = node.config.options.port; node.config.connectionTimeout = node.config.options.connectTimeout; node.config.requestTimeout = node.config.options.requestTimeout; node.config.cancelTimeout = node.config.options.cancelTimeout; node.config.encrypt = node.config.options.encrypt; node.connectedNodes = []; node.connectionCleanup = function (quiet) { const updateStatusAndLog = !quiet; try { if (node.poolConnect) { if (updateStatusAndLog) node.log(`Disconnecting server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); node.poolConnect.then(_ => _.close()).catch(e => { console.error(e); }); } } catch (error) { } // node-mssql 5.x to 6.x changes // ConnectionPool.close() now returns a promise / callbacks will be executed once closing of the if (node.pool && node.pool.close) { node.pool.close().catch(() => {}); } if (updateStatusAndLog) node.status({ fill: 'grey', shape: 'dot', text: 'disconnected' }); node.poolConnect = null; }; node.pool = new sql.ConnectionPool(node.config); node.pool.on('error', err => { node.error(err); node.connectionCleanup(); }); node.connect = function () { if (node.poolConnect) { return; } node.status({ fill: 'yellow', shape: 'dot', text: 'connecting' }); node.poolConnect = node.pool.connect(); return node.poolConnect; }; node.execSql = async function (queryMode, sqlQuery, params, paramValues, callback) { const _info = []; try { if (!node.poolConnect && !!(await node.connect())) { node.log(`Connected to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); } //FUTURE: let req = queryMode == "prepared" ? new sql.PreparedStatement(node.pool) : node.pool.request(); const req = node.pool.request(); req.on('info', info => { _info.push(info); }); //If bulk mode, create a table & populate the rows let bulkTable; if (queryMode === 'bulk') { bulkTable = new sql.Table(sqlQuery); bulkTable.create = true;//TODO: Present this as an option for (let rx = 0; rx < paramValues.length; rx++) { bulkTable.rows.push(paramValues[rx]); } } //setup params/columns if (params && params.length) { for (let index = 0; index < params.length; index++) { const p = params[index]; if (queryMode === 'bulk') { const _opts = p.options || {}; if (p.type && p.type.length) _opts.length = p.type.length; bulkTable.columns.add(p.name, p.type, _opts); continue; } if (p.output === true || p.output === 'true') { if (p.type) { req.output(p.name, p.type); } else { req.output(p.name); } } else { //if the data is a vtp/udt, coerce the type from string into sql.type if (p.type && (p.type.name === 'UDT' || p.type.name === 'TVP' || (p.type.type && p.type.type.name === 'TVP')) && typeof p.value === 'object' && p.value.columns && p.value.rows) { const table = new sql.Table(); for (let index = 0; index < p.value.columns.length; index++) { const col = p.value.columns[index]; table.columns.add(col.name, coerceType(col.type)); } for (let index = 0; index < p.value.rows.length; index++) { const row = p.value.rows[index]; if (Array.isArray(row)) { table.rows.add(...row); } else { table.rows.add(row); } } req.input(p.name, p.type, table); } else if (p.type) { req.input(p.name, p.type, p.value); } else { req.input(p.name, p.value); } } } } let result; switch (queryMode) { case 'bulk': result = await req.bulk(bulkTable); break; case 'execute': result = await req.execute(sqlQuery); break; // case "prepared": //FUTURE // await req.prepare(sqlQuery); // result = await req.execute(paramValues); // await req.unprepare(); // break; default: result = await req.query(sqlQuery); break; } callback(null, result, _info); } catch (e) { node.log(`Error connecting to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); console.error(e); node.poolConnect = null; callback(e); } }; node.disconnect = function (nodeId) { const index = node.connectedNodes.indexOf(nodeId); if (index >= 0) { node.connectedNodes.splice(index, 1); } if (node.connectedNodes.length === 0) { node.connectionCleanup(); } }; } RED.nodes.registerType('MSSQL-CN', connection, { credentials: { username: { type: 'text' }, password: { type: 'password' }, domain: { type: 'text' } } }); function mssql(config) { RED.nodes.createNode(this, config); const mssqlCN = RED.nodes.getNode(config.mssqlCN); const node = this; node.query = config.query; node.outField = config.outField || 'payload'; node.returnType = config.returnType; node.throwErrors = !(!config.throwErrors || config.throwErrors === '0'); node.params = config.params; node.queryMode = config.queryMode; node.modeOpt = config.modeOpt; node.modeOptType = config.modeOptType || 'query'; node.queryOpt = config.queryOpt; node.queryOptType = config.queryOptType || 'editor'; node.paramsOpt = config.paramsOpt; node.paramsOptType = config.paramsOptType || 'none'; node.rows = config.rows || 'rows'; node.rowsType = config.rowsType || 'msg'; node.parseMustache = !(config.parseMustache === false || config.parseMustache === 'false'); // if not explicitly set to false, then enable mustache parsing const setResult = function (msg, field, value, returnType = 0) { // eslint-disable-next-line eqeqeq const setValue = (returnType == 1 || msg.queryMode === 'bulk') ? value : value && value.recordset; const set = (obj, path, val) => { const keys = path.split('.'); const lastKey = keys.pop(); // eslint-disable-next-line no-return-assign const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, obj); lastObj[lastKey] = val; }; set(msg, field, setValue); }; const updateOutputParams = function (params, data) { if (!params || !params.length) return; if (!data || !data.output) return; const outputParams = params.filter(e => e.output); for (let index = 0; index < outputParams.length; index++) { const param = outputParams[index]; param.value = data.output[param.name]; } }; const validateQueryParam = function(p) { //{name:string, type?:string, value?:any, output?:boolean} if (typeof p !== 'object') { throw new Error('Parameter is not an object'); } if (!p.name || typeof p.name !== 'string') { throw new Error('Parameter does not have a valid name property'); } if (!p.output && !('value' in p)) { throw new Error('Input parameter \'' + p.name + '\' does not have a value property'); } if (p.type && p.type.toLowerCase() === 'uniqueidentifier') { if (!UUID.isValid(p.value)) { throw new Error('Unique identifier is not valid'); } } return true; }; const validateBulkColumn = function(p) { //{name:string, type?:string, value?:any, output?:boolean} if (typeof p !== 'object') { throw new Error('Column Parameter is not an object'); } if (!p.name || typeof p.name !== 'string') { throw new Error('Column Parameter does not have a valid name property'); } if (!p.type || typeof p.type !== 'string') { throw new Error('Column Parameter does not have a valid type property'); } if (p.type && p.type.toLowerCase() === 'uniqueidentifier') { if (!UUID.isValid(p.value)) { throw new Error('Unique identifier is not valid'); } } if (p.output) { throw new Error('Output parameter is not valid for bulk insert mode'); } return true; }; node.processError = function (err, msg) { let errMsg = 'Error'; if (typeof err === 'string') { errMsg = err; msg.error = err; } else if (err && err.message) { errMsg = err.message; if (err.precedingErrors !== undefined && err.precedingErrors.length && err.precedingErrors[0].originalError !== undefined && err.precedingErrors[0].originalError.info !== null && err.precedingErrors[0].originalError.info.message !== null) { errMsg += ' (' + err.precedingErrors[0].originalError.info.message + ')'; } //Make an error object from the err. NOTE: We cant just assign err to msg.error as a promise //rejection occurs when the node has 2 wires on the output. //(redUtil.cloneMessage(m) causes error "node-red Cannot assign to read only property 'originalError'") msg.error = { class: err.class, code: err.code, lineNumber: err.lineNumber, message: err.message, details: errMsg, name: err.name, number: err.number, procName: err.procName, serverName: err.serverName, state: err.state, toString: function () { return this.message; } }; } node.status({ fill: 'red', shape: 'ring', text: errMsg }); if (node.throwErrors) { node.error(msg.error, msg); } else { node.log(err); node.send(msg); } }; node.on('input', function (msg) { node.status({}); //clear node status delete msg.error; //remove any .error property passed in from previous node //evaluate UI typedInput settings... let queryMode; if (['query', 'execute', /*"prepared",*/'bulk'].includes(node.modeOptType)) { queryMode = node.modeOptType; } else { RED.util.evaluateNodeProperty(node.modeOpt, node.modeOptType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate query mode choice'; node.processError(errmsg, msg); } else { queryMode = value || 'query'; } }); } if (!['query', 'execute', /*"prepared",*/'bulk'].includes(queryMode)) { node.processError(`Query mode '${queryMode}' is not valid. Supported options are 'query' and 'execute'.`, msg); return null;//halt flow } let query; if (!node.paramsOptType || node.queryOptType === 'editor') { query = node.query || msg.payload; //legacy } else { RED.util.evaluateNodeProperty(node.queryOpt, node.queryOptType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate query choice'; node.processError(errmsg, msg); } else { query = value; } }); } let rows = null; if (queryMode === 'bulk') { if (node.paramsOptType === 'none') { node.processError('Columns must be provided in bulk mode', msg); return;//halt flow! } RED.util.evaluateNodeProperty(node.rows, node.rowsType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate rows field'; node.processError(errmsg, msg); } else { rows = value; } }); if (!rows || !Array.isArray(rows) || !rows.length || !(Array.isArray(rows[0]) || typeof rows[0] === 'object')) { node.processError('In bulk mode, rows must be an array of arrays or objects', msg); return;//halt flow! } } const queryParams = []; const queryParamValues = {}; if (node.paramsOptType === 'none') { //no params } else if (!node.paramsOptType || node.paramsOptType === 'editor') { const _params = node.params || []; for (let iParam = 0; iParam < _params.length; iParam++) { const param = RED.util.cloneMessage(_params[iParam]); if (param.output === false || param.output === 'false') { if (param.valueType === 'jsEpoch') { param.value = Date.now(); } else if (param.valueType === 'unixEpoch') { param.value = Math.floor(Date.now() / 1000); } else if (param.valueType === 'datetime') { param.value = new Date(); } else if (param.valueType === 'uuidv4') { param.value = UUID.v4(); } else { RED.util.evaluateNodeProperty(param.value, param.valueType, node, msg, (err, value) => { if (err) { const errmsg = `Unable to evaluate value for parameter at index [${iParam}] named '${param.name}'`; node.processError(errmsg, msg); } else { param.value = value; } }); } } queryParams.push(param); } } else { RED.util.evaluateNodeProperty(node.paramsOpt, node.paramsOptType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate parameter choice'; node.processError(errmsg, msg); } else { const _params = value || []; for (let iParam = 0; iParam < _params.length; iParam++) { const param = RED.util.cloneMessage(_params[iParam]); queryParams.push(param); } } }); } msg.query = query; msg.queryMode = queryMode; msg.queryParams = queryParams || []; //now validate params, remove superfluous properties & coerce type into sql.type if (msg.queryParams && msg.queryParams.length) { if (queryMode === 'bulk') { for (let iParam = 0; iParam < msg.queryParams.length; iParam++) { const param = msg.queryParams[iParam]; try { validateBulkColumn(param); param.type = coerceType(param.type); } catch (error) { node.processError(`Column parameter at index [${iParam}] is not valid. ${error.message}.`, msg); return null; } } } else { for (let iParam = 0; iParam < msg.queryParams.length; iParam++) { const param = msg.queryParams[iParam]; try { validateQueryParam(param); param.type = coerceType(param.type); if (param.output) { delete param.value; } else { queryParamValues[param.name] = param.value; } delete param.valueType; } catch (error) { node.processError(`query parameter at index [${iParam}] is not valid. ${error.message}.`, msg); return null; } } } } const promises = []; const resolvedTokens = {}; let tokens; if (node.parseMustache) { tokens = extractTokens(mustache.parse(msg.query)); tokens.forEach(function (name) { const envName = parseEnv(name); if (envName) { const promise = new Promise((resolve, reject) => { const val = RED.util.evaluateNodeProperty(envName, 'env', node); resolvedTokens[name] = val; resolve(); }); promises.push(promise); return; } const context = parseContext(name); if (context) { const type = context.type; const store = context.store; const field = context.field; const target = node.context()[type]; if (target) { const promise = new Promise((resolve, reject) => { target.get(field, store, (err, val) => { if (err) { reject(err); } else { resolvedTokens[name] = val; resolve(); } }); }); promises.push(promise); } } }); } if (tokens && tokens.size > 0) { Promise.all(promises).then(function () { const value = mustache.render(msg.query, new NodeContext(msg, node.context(), null, false, resolvedTokens)); msg.query = value; const values = msg.queryMode === 'bulk' ? rows : queryParamValues; doSQL(node, msg, values); }).catch(function (err) { node.processError(err, msg); }); } else { const values = msg.queryMode === 'bulk' ? rows : queryParamValues; doSQL(node, msg, values); } }); function doSQL(node, msg, values) { node.status({ fill: 'blue', shape: 'dot', text: 'requesting' }); try { mssqlCN.execSql(msg.queryMode, msg.query, msg.queryParams, values, function (err, data, info) { if (err) { node.processError(err, msg); } else { node.status({ fill: 'green', shape: 'dot', text: 'done' }); msg.sqlInfo = info; updateOutputParams(msg.queryParams, data); setResult(msg, node.outField, data, node.returnType); node.send(msg); } }); } catch (err) { node.processError(err, msg); } } node.on('close', function () { mssqlCN.disconnect(node.id); }); } RED.nodes.registerType('MSSQL', mssql); };