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...
+
+
可以透過發送包含 command
的 topic
和包含 connect
的 payload
來手動控制與資料庫的連接代碼> 或<代碼>斷開代碼>。這對於在運行時強制連線狀態很有用。
+
+ 可以透過新增指向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...
-
可以透過發送包含 command
的 topic
和包含 connect
的 payload
來手動控制與資料庫的連接代碼> 或<代碼>斷開代碼>。這對於在運行時強制連線狀態很有用。
+
可以透過發送包含 command
的 topic
和包含 connect
或 disconnect
的 payload
代碼來手動控制資料庫的連接。這對於在執行時期強制變動連線狀態很有用處。
- 可以透過新增指向MSSQL節點的complete
節點或catch
節點來確定此操作的結果
+ 可以透過新增指向 MSSQL 節點的 complete
節點或 catch
節點來確定此操作的結果
查詢模式...