From feb7c8908cef56f4fec59672a2a6cacdb9ce8511 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 1 Sep 2024 21:46:09 +0100 Subject: [PATCH 1/4] support connect/disconnect on demand --- src/mssql.js | 254 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 171 insertions(+), 83 deletions(-) diff --git a/src/mssql.js b/src/mssql.js index b66b838..6368873 100755 --- a/src/mssql.js +++ b/src/mssql.js @@ -267,9 +267,19 @@ module.exports = function (RED) { } function connection (config) { + // implement an eventBus (separate to the node-red provided emitter) to allow for status updates + const EventEmitter = require('events') + RED.nodes.createNode(this, config) const node = this + node.eventBus = new EventEmitter() + // add default listeners to prevent unhandled exceptions + node.eventBus.on('error', () => {}) + node.eventBus.on('connected', () => {}) + node.eventBus.on('connecting', () => {}) + node.eventBus.on('disconnected', () => {}) + // add mustache transformation to connection object const configStr = JSON.stringify(config) const transform1 = mustache.render(configStr, process.env) @@ -321,43 +331,41 @@ module.exports = function (RED) { node.config.encrypt = node.config.options.encrypt node.connectedNodes = [] + /** @type {Promise} */ + node.poolConnect = null + /** @type {sql.ConnectionPool} */ + node.pool = null + + /** @returns {Promise} */ + node.connect = function () { + if (!node.pool) { + node.pool = new sql.ConnectionPool(node.config) + node.pool.on('error', node.onPoolError) + } + if (node.pool && (node.pool.connected || node.pool.connecting)) { + return node.poolConnect + } + node.poolConnect = node.pool.connect() + return node.poolConnect + } - node.connectionCleanup = function (quiet) { + node.connectionCleanup = async 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) }) + if (node.isConnectedOrConnecting()) { + node.pool.off('error', node.onPoolError) + node.pool.removeAllListeners() + await node.pool.close() } } catch (error) { + console.warn('Error closing connection pool:', 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.pool = null + node.connectedNodes = [] + if (updateStatusAndLog) { + node.eventBus.emit('disconnected') } - 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) { @@ -451,15 +459,43 @@ module.exports = function (RED) { } } - node.disconnect = function (nodeId) { + /** + * Remove a node from the list of connected nodes. + * If `nodeId` is `undefined`, all nodes that use this connection are disconnected. + * @param {String} [nodeId] - ID of the node disconnecting + */ + node.disconnect = async function (nodeId) { + if (nodeId === undefined) { + await node.connectionCleanup() + return + } const index = node.connectedNodes.indexOf(nodeId) if (index >= 0) { node.connectedNodes.splice(index, 1) } if (node.connectedNodes.length === 0) { - node.connectionCleanup() + await node.connectionCleanup() } } + + node.onPoolError = async function (err) { + node.error(err) + node.eventBus.emit('error', err) + await node.connectionCleanup() + } + + node.isConnected = function () { + return node.pool && node.pool.connected + } + + node.isConnectedOrConnecting = function () { + return node.pool && (node.pool.connected || node.pool.connecting) + } + + node.on('close', async done => { + await node.connectionCleanup(true) + done() + }) } RED.nodes.registerType('MSSQL-CN', connection, { @@ -478,8 +514,36 @@ module.exports = function (RED) { function mssql (config) { RED.nodes.createNode(this, config) + /** @type {connection} */ const mssqlCN = RED.nodes.getNode(config.mssqlCN) const node = this + node.status({ fill: 'grey', shape: 'ring', text: 'ready' }) + + if (!mssqlCN) { + node.status({ fill: 'red', shape: 'ring', text: 'missing connection' }) + return + } + + node.statusConnected = function () { + node.status({ fill: 'green', shape: 'dot', text: 'connected' }) + } + + node.statusDisconnected = function () { + node.status({}) + } + + node.statusError = function (message) { + node.status({ fill: 'red', shape: 'ring', text: message }) + } + + node.statusConnecting = function () { + node.status({ fill: 'yellow', shape: 'ring', text: 'connecting' }) + } + + mssqlCN.eventBus.on('connected', node.statusConnected) + mssqlCN.eventBus.on('connecting', node.statusConnecting) + mssqlCN.eventBus.on('disconnected', node.statusDisconnected) + mssqlCN.eventBus.on('error', node.statusError) node.query = config.query node.outField = config.outField || 'payload' @@ -563,7 +627,7 @@ module.exports = function (RED) { return true } - node.processError = function (err, msg) { + node.processError = function (err, msg, send, done, notifyAll) { let errMsg = 'Error' if (typeof err === 'string') { errMsg = err @@ -592,30 +656,55 @@ module.exports = function (RED) { procName: err.procName, serverName: err.serverName, state: err.state, + stack: err.stack, toString: function () { return this.message } } } - - node.status({ - fill: 'red', - shape: 'ring', - text: errMsg - }) - + if (notifyAll) { + mssqlCN.eventBus.emit('error', errMsg) + } else { + node.status({ fill: 'red', shape: 'ring', text: errMsg }) + } if (node.throwErrors) { - node.error(msg.error, msg) + done(msg.error) } else { node.log(err) - node.send(msg) + send(msg) + done() } } - node.on('input', function (msg) { + node.on('input', async function (msg, send, done) { node.status({}) // clear node status delete msg.error // remove any .error property passed in from previous node + // determine if the user is requesting a command to control the connection + const controlCommands = ['connect', 'disconnect'] + if (msg.topic === 'command' && controlCommands.includes(msg.payload)) { + switch (msg.payload) { + case 'connect': + try { + mssqlCN.eventBus.emit('connecting') + await mssqlCN.connect() + mssqlCN.eventBus.emit('connected') + done() + } catch (error) { + node.processError(error, msg, send, done, true) + } + return + case 'disconnect': + if (!mssqlCN.poolConnect) { + mssqlCN.eventBus.emit('disconnected') + } else { + await mssqlCN.disconnect(undefined) + } + done() + return + } + } + // evaluate UI typedInput settings... let queryMode if (['query', 'execute', /* "prepared", */'bulk'].includes(node.modeOptType)) { @@ -624,7 +713,7 @@ module.exports = function (RED) { 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) + node.processError(errmsg, msg, send, done) } else { queryMode = value || 'query' } @@ -632,7 +721,7 @@ module.exports = function (RED) { } if (!['query', 'execute', /* "prepared", */'bulk'].includes(queryMode)) { - node.processError(`Query mode '${queryMode}' is not valid. Supported options are 'query' and 'execute'.`, msg) + node.processError(`Query mode '${queryMode}' is not valid. Supported options are 'query' and 'execute'.`, msg, send, done) return null// halt flow } @@ -643,7 +732,7 @@ module.exports = function (RED) { RED.util.evaluateNodeProperty(node.queryOpt, node.queryOptType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate query choice' - node.processError(errmsg, msg) + node.processError(errmsg, msg, send, done) } else { query = value } @@ -653,19 +742,19 @@ module.exports = function (RED) { let rows = null if (queryMode === 'bulk') { if (node.paramsOptType === 'none') { - node.processError('Columns must be provided in bulk mode', msg) + node.processError('Columns must be provided in bulk mode', msg, send, done) 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) + node.processError(errmsg, msg, send, done) } 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) + node.processError('In bulk mode, rows must be an array of arrays or objects', msg, send, done) return// halt flow! } } @@ -691,7 +780,7 @@ module.exports = function (RED) { 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) + node.processError(errmsg, msg, send, done) } else { param.value = value } @@ -704,7 +793,7 @@ module.exports = function (RED) { RED.util.evaluateNodeProperty(node.paramsOpt, node.paramsOptType, node, msg, (err, value) => { if (err) { const errmsg = 'Unable to evaluate parameter choice' - node.processError(errmsg, msg) + node.processError(errmsg, msg, send, done) } else { const _params = value || [] for (let iParam = 0; iParam < _params.length; iParam++) { @@ -728,7 +817,7 @@ module.exports = function (RED) { validateBulkColumn(param) param.type = coerceType(param.type) } catch (error) { - node.processError(`Column parameter at index [${iParam}] is not valid. ${error.message}.`, msg) + node.processError(`Column parameter at index [${iParam}] is not valid. ${error.message}.`, msg, send, done) return null } } @@ -745,7 +834,7 @@ module.exports = function (RED) { } delete param.valueType } catch (error) { - node.processError(`query parameter at index [${iParam}] is not valid. ${error.message}.`, msg) + node.processError(`query parameter at index [${iParam}] is not valid. ${error.message}.`, msg, send, done) return null } } @@ -789,50 +878,49 @@ module.exports = function (RED) { } } }) - + node.status({ fill: 'blue', shape: 'dot', text: 'requesting' }) 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) + mssqlCN.execSql(msg.queryMode, msg.query, msg.queryParams, values, execSqlResultHandler) }).catch(function (err) { - node.processError(err, msg) + node.processError(err, msg, send, done) }) } else { + node.status({ fill: 'blue', shape: 'dot', text: 'requesting' }) const values = msg.queryMode === 'bulk' ? rows : queryParamValues - doSQL(node, msg, values) + mssqlCN.execSql(msg.queryMode, msg.query, msg.queryParams, values, execSqlResultHandler) } - }) - function doSQL (node, msg, values) { - node.status({ - fill: 'blue', - shape: 'dot', - text: 'requesting' - }) + function execSqlResultHandler (err, data, info) { + if (err) { + node.processError(err, msg, send, done) + } else { + node.status({ + fill: 'green', + shape: 'dot', + text: 'done' + }) + msg.sqlInfo = info + updateOutputParams(msg.queryParams, data) + setResult(msg, node.outField, data, node.returnType) + send(msg) + done() + } + } + }) + node.on('close', async function () { 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) + mssqlCN.eventBus.removeListener('connected', node.statusConnected) + mssqlCN.eventBus.removeListener('connecting', node.statusConnecting) + mssqlCN.eventBus.removeListener('disconnected', node.statusDisconnected) + mssqlCN.eventBus.removeListener('error', node.statusError) + await mssqlCN.disconnect(node.id) + } catch (error) { + console.error('Error closing node', error) } - } - node.on('close', function () { - mssqlCN.disconnect(node.id) }) } RED.nodes.registerType('MSSQL', mssql) From b362f36b1c2349a56d3a7ee3ac2010da166aa24d Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 1 Sep 2024 21:46:33 +0100 Subject: [PATCH 2/4] update build in help - detailing connect/disconnect commands --- src/locales/en-US/mssql.html | 19 +++++++++++++------ src/locales/zh-TW/mssql.html | 8 +++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/locales/en-US/mssql.html b/src/locales/en-US/mssql.html index 52120c3..bfcec4b 100644 --- a/src/locales/en-US/mssql.html +++ b/src/locales/en-US/mssql.html @@ -21,8 +21,15 @@

Foreword

Examples have been included to help you do some common tasks. Click here to import an example or press the hamburger menu select import then examples or press ctrl+i -
+ +

Commands...

+
+

The connection to the database can be manually controlled by sending a topic containing command and a payload containing either connect or disconnect. This can be useful for forcing connection state at runtime.

+
+ The result of this operation can be determined by adding a complete node or a catch node pointed at the MSSQL node. +
+

Query Mode...

Select the execution mode, this can be "Query", "Stored procedure" or "Bulk Insert"

@@ -32,7 +39,7 @@

Query Mode...

INFO: TVP variables are only supported in stored procedures. Some variable types are not supported by the underlying SQL driver.

- +

Query...

Enter the query or stored procedure name to execute. It is possible to use mustache format to access properties of the msg, flow context and global context.

@@ -88,7 +95,7 @@

Query...

- +

Parameters...

Input and Output Parameters can be specified for a query or procedure. In bulk mode, the parameters represent the columns of the table

@@ -118,8 +125,8 @@

Parameters...

Parameters -
    -
  • +
      +
    • In/Out input , Name name , Type varchar(20) @@ -135,7 +142,7 @@

      Parameters...

- +

Output options...

Output property

diff --git a/src/locales/zh-TW/mssql.html b/src/locales/zh-TW/mssql.html index 3e4b408..f4b1f7e 100644 --- a/src/locales/zh-TW/mssql.html +++ b/src/locales/zh-TW/mssql.html @@ -18,7 +18,13 @@

Foreword

Examples have been included to help you do some common tasks. Click here to import an example or press the hamburger menu select import then examples or press ctrl+i - +

Commands...

+
+

可以透過發送包含 commandtopic 和包含 connectpayload 來手動控制與資料庫的連接代碼> 或<代碼>斷開。這對於在運行時強制連線狀態很有用。

+
+ 可以透過新增指向MSSQL節點的complete節點或catch節點來確定此操作的結果 +
+

查詢模式...

選擇查詢模式, 可以為 "Query" 或是 "Stored procedure"

From 7baa2261cbc1a335ca4da9873006a9bbd0cfebc5 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 1 Sep 2024 22:52:00 +0100 Subject: [PATCH 3/4] update unit tests --- test/_config.test.json | 4 +- test/mssql-plus_spec.js | 233 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 220 insertions(+), 17 deletions(-) diff --git a/test/_config.test.json b/test/_config.test.json index 4dc922e..74b4503 100644 --- a/test/_config.test.json +++ b/test/_config.test.json @@ -8,7 +8,7 @@ "tdsVersion": "7_4", "trustServerCertificate": true, "useUTC": true, - "connectTimeout": 15000, - "requestTimeout": 15000, + "connectTimeout": 5000, + "requestTimeout": 5000, "cancelTimeout": 5000 } \ No newline at end of file diff --git a/test/mssql-plus_spec.js b/test/mssql-plus_spec.js index 4381782..0242faf 100644 --- a/test/mssql-plus_spec.js +++ b/test/mssql-plus_spec.js @@ -62,26 +62,150 @@ describe('Load MSSQL Plus Node', function () { const helperNode = helper.getNode('helperNode') const sqlNode = helper.getNode('sqlNode') const configNode = helper.getNode('configNode') + try { + should(helperNode).not.be.undefined() + should(sqlNode).not.be.undefined() + should(configNode).not.be.undefined() - should(helperNode).not.be.undefined() - should(sqlNode).not.be.undefined() - should(configNode).not.be.undefined() + sqlNode.should.have.property('type', 'MSSQL') + // ensure defaults are sane + sqlNode.should.have.property('modeOpt').and.be.undefined() + sqlNode.should.have.property('modeOptType', 'query') + sqlNode.should.have.property('outField', 'payload') // compatibility with original node + sqlNode.should.have.property('params').and.be.undefined() + sqlNode.should.have.property('paramsOpt').and.be.undefined() + sqlNode.should.have.property('paramsOptType', 'none') + sqlNode.should.have.property('parseMustache', true) // compatibility with original node + sqlNode.should.have.property('query').and.be.undefined() + sqlNode.should.have.property('queryMode').and.be.undefined() + sqlNode.should.have.property('queryOpt').and.be.undefined() + sqlNode.should.have.property('queryOptType', 'editor') // compatibility with original node + sqlNode.should.have.property('returnType').and.be.undefined() + sqlNode.should.have.property('throwErrors', false) // compatibility with original node + sqlNode.should.have.property('rows', 'rows') + sqlNode.should.have.property('rowsType', 'msg') - sqlNode.should.have.property('type', 'MSSQL') - sqlNode.should.have.property('modeOptType', 'query') + configNode.should.have.property('type', 'MSSQL-CN') + configNode.should.have.property('config') + configNode.should.have.property('pool') - configNode.should.have.property('config') - configNode.should.have.property('pool') - configNode.should.have.property('type', 'MSSQL-CN') - - done() + done() + } catch (error) { + done(error) + } }) }) // Dynamic Tests // TODO: expose internal functionality (like row/column creation, coerceType helper functions to permit testing) + it('should connect to database when topic and payload are set', function (done) { + this.timeout((testConnectionConfig.connectTimeout || 5000) + 2000) // timeout with an error if done() isn't called within allotted time + + const cn = getConfigNode('configNode', testConnectionConfig) + // flow that contains a status and catch node + const flow = [ + cn, + { id: 'helperNode', type: 'helper' }, + { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, wires: [['helperNode']] }, + { id: 'catchNode', type: 'catch', name: 'catch', scope: ['sqlNode'], wires: [['helperNodeCatch']] }, + { id: 'helperNodeCatch', type: 'helper' }, + { id: 'completeNode', type: 'complete', name: '', scope: ['sqlNode'], uncaught: false, wires: [['helperNodeComplete']] }, + { id: 'helperNodeComplete', type: 'helper' } + ] + + helper.load(mssqlPlusNode, flow, function () { + const helperNode = helper.getNode('helperNode') + const helperNodeCatch = helper.getNode('helperNodeCatch') + const helperNodeComplete = helper.getNode('helperNodeComplete') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') + + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + + helperNodeComplete.on('input', function (msg) { + try { + msg.should.have.property('topic', 'command') + msg.should.have.property('payload', 'connect') + msg.should.not.have.property('error') + done() + } catch (error) { + done(error) + } + }) + helperNodeCatch.on('input', function (msg) { + done(new Error('did not expect the mssql node to throw an error')) + }) + helperNode.on('input', function (msg) { + done(new Error('did not expect the mssql node to output a message')) + }) + sqlNode.receive({ topic: 'command', payload: 'connect' }) // fire input of testNode + }) + }) + + it('should disconnect from database when topic and payload are set', function (done) { + this.timeout((testConnectionConfig.connectTimeout || 5000) + 2000) // timeout with an error if done() isn't called within allotted time + + const cn = getConfigNode('configNode', testConnectionConfig) + // flow that contains a status and catch node + const flow = [ + cn, + { id: 'helperNode', type: 'helper' }, + { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, wires: [['helperNode']] }, + { id: 'catchNode', type: 'catch', name: 'catch', scope: ['sqlNode'], wires: [['helperNodeCatch']] }, + { id: 'helperNodeCatch', type: 'helper' }, + { id: 'completeNode', type: 'complete', name: '', scope: ['sqlNode'], uncaught: false, wires: [['helperNodeComplete']] }, + { id: 'helperNodeComplete', type: 'helper' } + ] + + helper.load(mssqlPlusNode, flow, async function () { + const helperNode = helper.getNode('helperNode') + const helperNodeCatch = helper.getNode('helperNodeCatch') + const helperNodeComplete = helper.getNode('helperNodeComplete') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') + + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + // set connection timeout to 0.9 seconds + configNode.config.connectTimeout = '900' + + sqlNode.receive({ topic: 'command', payload: 'connect' }) // connect to db + + // wait for the connection to be established + await new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 1500) + }) + + // check isConnected status + const isConnected = configNode.isConnected() + should(isConnected).be.true('expected connection to be established') + + // now hook up events and call disconnect + helperNodeComplete.on('input', function (msg) { + try { + msg.should.have.property('topic', 'command') + msg.should.have.property('payload', 'disconnect') + msg.should.not.have.property('error') + done() + } catch (error) { + done(error) + } + }) + helperNodeCatch.on('input', function (msg) { + done(new Error('did not expect the mssql node to throw an error')) + }) + helperNode.on('input', function (msg) { + done(new Error('did not expect the mssql node to output a message')) + }) + sqlNode.receive({ topic: 'command', payload: 'disconnect' }) // fire input of testNode + }) + }) + it('should perform a simple query', function (done) { - this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000) // timeout with an error if done() isn't called within alloted time + this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000) // timeout with an error if done() isn't called within allotted time const cn = getConfigNode('configNode', testConnectionConfig) const flow = [ @@ -98,8 +222,6 @@ describe('Load MSSQL Plus Node', function () { configNode.config.user = testConnectionConfig.username configNode.config.password = testConnectionConfig.password - configNode.pool.config.user = testConnectionConfig.username - configNode.pool.config.password = testConnectionConfig.password configNode.should.have.property('id', 'configNode') helperNode.on('input', function (msg) { @@ -110,6 +232,46 @@ describe('Load MSSQL Plus Node', function () { should(msg.payload.length).eql(1, 'payload array must have 1 element') msg.payload[0].should.have.property('now') should(msg.payload[0].now).not.be.undefined() + msg.should.not.have.property('error') + done() + } catch (error) { + done(error) + } + }) + + sqlNode.receive({ payload: query }) // fire input of testNode + }) + }) + + it('should return data to specified property', function (done) { + this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000) // timeout with an error if done() isn't called within allotted time + + const cn = getConfigNode('configNode', testConnectionConfig) + const flow = [ + cn, + { id: 'helperNode', type: 'helper' }, + { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, outField: 'custom_output', wires: [['helperNode']] } + ] + + helper.load(mssqlPlusNode, flow, function () { + const query = 'SELECT GETDATE() as now' + const helperNode = helper.getNode('helperNode') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') + + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + + configNode.should.have.property('id', 'configNode') + helperNode.on('input', function (msg) { + try { + msg.should.have.property('query', query) + msg.should.have.property('custom_output') + should(Array.isArray(msg.custom_output)).be.true('custom_output must be an array') + should(msg.custom_output.length).eql(1, 'custom_output array must have 1 element') + msg.custom_output[0].should.have.property('now') + should(msg.custom_output[0].now).not.be.undefined() + msg.should.not.have.property('error') done() } catch (error) { done(error) @@ -120,6 +282,49 @@ describe('Load MSSQL Plus Node', function () { }) }) + it('should perform a simple using ui configured query with mustache', function (done) { + this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000) // timeout with an error if done() isn't called within allotted time + const cn = getConfigNode('configNode', testConnectionConfig) + const flow = [ + cn, + { id: 'helperNode', type: 'helper' }, + { id: 'sqlNode', type: 'MSSQL', mssqlCN: cn.id, name: '', outField: 'payload', throwErrors: '1', query: "SELECT GETDATE() as now, {{payload.number}} as anum, '{{payload.string}}' as astr\r\n", modeOpt: 'queryMode', modeOptType: 'query', queryOpt: 'payload', queryOptType: 'editor', paramsOpt: 'queryParams', paramsOptType: 'none', rows: 'rows', rowsType: 'msg', parseMustache: true, params: [], wires: [['helperNode']] } + ] + + helper.load(mssqlPlusNode, flow, function () { + const helperNode = helper.getNode('helperNode') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') + + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + + configNode.should.have.property('id', 'configNode') + helperNode.on('input', function (msg) { + try { + msg.should.have.property('query').and.be.a.String() + msg.should.have.property('payload') + should(Array.isArray(msg.payload)).be.true('payload must be an array') + should(msg.payload.length).eql(1, 'payload array must have 1 element') + msg.payload[0].should.have.property('now') + should(msg.payload[0].now).not.be.undefined() + msg.payload[0].should.have.property('anum', 42) + msg.payload[0].should.have.property('astr', 'hello') + done() + } catch (error) { + done(error) + } + }) + + sqlNode.receive({ + payload: { + number: 42, + string: 'hello' + } + }) // fire input of testNode + }) + }) + it('should can create table and insert/select data', function (done) { const cn = getConfigNode('configNode', testConnectionConfig) @@ -137,8 +342,6 @@ describe('Load MSSQL Plus Node', function () { configNode.config.user = testConnectionConfig.username configNode.config.password = testConnectionConfig.password - configNode.pool.config.user = testConnectionConfig.username - configNode.pool.config.password = testConnectionConfig.password configNode.should.have.property('id', 'configNode') From 31e1bd3e784a3997f56c513d032af699ee2b567d Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:32:56 +0100 Subject: [PATCH 4/4] Update src/locales/zh-TW/mssql.html Co-authored-by: Shao Yu-Lung (Allen) --- src/locales/zh-TW/mssql.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/zh-TW/mssql.html b/src/locales/zh-TW/mssql.html index f4b1f7e..9a76c0d 100644 --- a/src/locales/zh-TW/mssql.html +++ b/src/locales/zh-TW/mssql.html @@ -20,9 +20,9 @@

Foreword

Commands...

-

可以透過發送包含 commandtopic 和包含 connectpayload 來手動控制與資料庫的連接代碼> 或<代碼>斷開。這對於在運行時強制連線狀態很有用。

+

可以透過發送包含 commandtopic 和包含 connectdisconnectpayload 代碼來手動控制資料庫的連接。這對於在執行時期強制變動連線狀態很有用處。

- 可以透過新增指向MSSQL節點的complete節點或catch節點來確定此操作的結果 + 可以透過新增指向 MSSQL 節點的 complete 節點或 catch 節點來確定此操作的結果

查詢模式...