diff --git a/package.json b/package.json index a6cbec9e6..6e96db76a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "poplib" : "^0.1.7", "mailparser" : "^0.6.1", "imap" : "^0.8.18", - "msgpack-js": "^0.3.0" + "msgpack-js": "^0.3.0", + "mysql": "^2.13.0" }, "engines": { "node": ">=0.10" diff --git a/storage/mysql/68-mysql.html b/storage/mysql/68-mysql.html index 85abf6b73..7bb068fc0 100644 --- a/storage/mysql/68-mysql.html +++ b/storage/mysql/68-mysql.html @@ -53,41 +53,85 @@ +
+ + + +
+
+ + +
+
+
+
diff --git a/storage/mysql/68-mysql.js b/storage/mysql/68-mysql.js index a3362fa50..ae29dee1d 100644 --- a/storage/mysql/68-mysql.js +++ b/storage/mysql/68-mysql.js @@ -102,6 +102,8 @@ module.exports = function(RED) { RED.nodes.createNode(this,n); this.mydb = n.mydb; this.mydbConfig = RED.nodes.getNode(this.mydb); + this.query = n.query; + this.parameterSource = n.parameterSource || 'payload'; if (this.mydbConfig) { this.mydbConfig.connect(); @@ -118,24 +120,67 @@ module.exports = function(RED) { node.on("input", function(msg) { if (node.mydbConfig.connected) { - if (typeof msg.topic === 'string') { - //console.log("query:",msg.topic); - var bind = Array.isArray(msg.payload) ? msg.payload : []; - node.mydbConfig.connection.query(msg.topic, bind, function(err, rows) { - if (err) { - node.error(err,msg); - node.status({fill:"red",shape:"ring",text:"Error"}); - } - else { - msg.payload = rows; - node.send(msg); - node.status({fill:"green",shape:"dot",text:"OK"}); - } - }); + node.status({fill:"green",shape:"dot",text:"connected"}); + + // Query to be executed + var query = node.query; + + // Array of input parameters + var parameters = []; + + if(query.length){ + // Search for all paramters in a query + var parametersUsed = node.query.match(/\{\{[A-z\.0-9]*?\}\}/g); + var parameterSourcePath = node.parameterSource.split('.'); + + var sourceObject = msg; + + // Defaults to top level + var parameterSourceKey = parameterSourcePath.shift(); + while(parameterSourceKey){ + sourceObject = sourceObject[parameterSourceKey]; + + parameterSourceKey = parameterSourcePath.shift(); + } + // Loop matched parameters in query + if(parametersUsed) { + for(var i=0; i < parametersUsed.length; i++){ + var parameter = parametersUsed[i]; + query = query.replace(parameter,'?'); + + // Clean out {{}} characters and create a dot deliminated array of keys to traverse. + var parameterPath = parameter.replace(/[^A-z\.0-9]/g,'') + .split('.'); + + // Default to key + var value = sourceObject; + var parameterPathKey = parameterPath.shift(); + while(parameterPathKey){ + value = value[parameterPathKey]; + parameterPathKey = parameterPath.shift(); + } + + // Add to our parameter array for query execution + parameters.push(value); + } + } } - else { - if (typeof msg.topic !== 'string') { node.error("msg.topic : the query is not defined as a string"); } + else if (typeof msg.topic === 'string') { + parameters = Array.isArray(msg.payload) ? msg.payload : []; + query = msg.topic; } + + node.mydbConfig.connection.query(query, parameters, function(err, rows) { + if (err) { + node.error(err,msg); + node.status({fill:"red",shape:"ring",text:"Error"}); + } + else { + msg.payload = rows; + node.send(msg); + node.status({fill:"green",shape:"dot",text:"OK"}); + } + }); } else { node.error("Database not connected",msg); diff --git a/storage/mysql/README.md b/storage/mysql/README.md index b3b769304..8e0ac9587 100644 --- a/storage/mysql/README.md +++ b/storage/mysql/README.md @@ -15,15 +15,38 @@ Usage Allows basic access to a MySQL database. -This node uses the query operation against the configured database. This does allow both INSERTS and DELETES. +This node uses the query operation against the configured database. This does allow both INSERTS and DELETES. -By it's very nature it allows SQL injection... so be careful out there... +Using legacy method where queries are set in `msg.topic` allows SQL injection... so be careful out there... -The `msg.topic` must hold the query for the database, and the result is returned in `msg.payload`. +###Direct Inserted Queries + +With SQL queries that are directly added on the node, variables are escaped. + +SQL queries can use mustache style variable insertion. If our `msg.payload` has a property `key`, we would write a query as following: + +``` + SELECT * + FROM table + WHERE column = {{key}}; +``` + +For more escaped input information, you can refer to the documentation for [mysqljs/mysql](https://github.com/mysqljs/mysql). + +###Results Typically the returned payload will be an array of the result rows. -If nothing is found for the key then null is returned. +If nothing is found for the key then null is returned, + +###Misc. + +The reconnect timeout in milliseconds can be changed by adding a line to settings.js + +`mysqlReconnectTime: 30000,` + +###Legacy + +`msg.topic` must hold the query for the database, and the result is returned in `msg.payload`. -The reconnect retry timeout in milliseconds can be changed by adding a line to settings.js -
mysqlReconnectTime: 30000,

+`msg.payload` can contain an array of values to bind to the topic. diff --git a/storage/mysql/icons/sql.png b/storage/mysql/icons/sql.png new file mode 100644 index 000000000..6a3944a02 Binary files /dev/null and b/storage/mysql/icons/sql.png differ diff --git a/storage/mysql/package.json b/storage/mysql/package.json index 8de0fc77a..5dda21bd2 100644 --- a/storage/mysql/package.json +++ b/storage/mysql/package.json @@ -20,5 +20,21 @@ "name": "Dave Conway-Jones", "email": "ceejay@vnet.ibm.com", "url": "http://nodered.org" + }, + "contributor": [ + { + "name": "RyanSH100", + "url": "https://github.com/ryansh100" + } + ], + "scripts": { + "test": "mocha '../../test/storage/mysql/mysql_spec.js'" + }, + "devDependencies": { + "mocha": "^3.2.0", + "proxyquire": "^1.7.10", + "should": "^11.1.2", + "should-sinon": "0.0.5", + "sinon": "^1.17.7" } } diff --git a/test/storage/mysql/mysql_spec.js b/test/storage/mysql/mysql_spec.js new file mode 100644 index 000000000..2816f9b50 --- /dev/null +++ b/test/storage/mysql/mysql_spec.js @@ -0,0 +1,129 @@ +var sinon = require('sinon'); +var should = require('should'); +var proxyquire = require('proxyquire'); +var helper = require('../../../test/helper.js'); +var mysqlConnection; +var mysqlConnectionPool = { + getConnection(callback) { + callback(null, mysqlConnection); + } +} +var mysqlMock = { + createPool: function () { + return mysqlConnectionPool; + } +} + +var mysqlNode = proxyquire('../../../storage/mysql/68-mysql.js', + { 'mysql': mysqlMock }); + +describe('test MySql Node', function() { + var flow = [ { + id:"n1", + type:"mysql", + name:"mysql", + mydb: "mysqlConfig", + wires:[[]], + query: "SELECT * FROM tables WHERE columnB = {{B}} AND columnA IN ({{A}})" + }, + { + id: 'mysqlConfig', + type: 'MySQLdatabase', + host: 'some-host', + port: 'some-port' + }]; + var creds = { + "mysqlConfig": { + user: 'some-user', + password: 'some-pass' + } + } + var queryNode; + + beforeEach(function(done){ + helper.load(mysqlNode, flow, creds, function() { + queryNode = helper.getNode('n1'); + done(); + }); + mysqlConnection = { + connect: function(callback){ + callback(); + }, + on: sinon.spy(), + query: sinon.spy() + }; + }); + afterEach(function(done) { + helper.unload().then(done); + }); + it('can make a simple query', function(done) { + queryNode.query = "SELECT * FROM table"; + queryNode.receive({ + topic: 'some-topic', + payload: "A Message" + }); + + setTimeout(function() { + mysqlConnection.query.calledWith( + "SELECT * FROM table", + []).should.be.true(); + done(); + }, 25); + }); + + it('can accept moustache style variables in query', function(done) { + queryNode.receive({payload:{"A":"ParamA", "B":"ParamB"}}); + + setTimeout(function() { + mysqlConnection.query.calledWith( + "SELECT * FROM tables WHERE columnB = ? AND columnA IN (?)", + [ + "ParamB", + "ParamA" + ]).should.be.true(); + done(); + }, 25); + }); + + it('can use deeply nested properties for variable source', function(done) { + queryNode.parameterSource = "payload.deeply.nested"; + queryNode.receive({ + payload: { + deeply: { + nested: { + "A": "ParamA", + "B": "ParamB" + } + } + } + }); + + setTimeout(function() { + mysqlConnection.query.calledWith( + "SELECT * FROM tables WHERE columnB = ? AND columnA IN (?)", + [ + "ParamB", + "ParamA" + ]).should.be.true(); + done(); + }, 25); + }); + + it('can fallback to topic based queries', function(done) { + queryNode.query = ""; + queryNode.receive({ + topic: 'SELECT * FROM table', + payload: { + "A": "ParamA", + "B": "ParamB" + } + }); + + setTimeout(function() { + mysqlConnection.query.calledWith( + "SELECT * FROM table", + []).should.be.true(); + done(); + }, 25); + }); +});