Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mysql node update for holding query directly. #297

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
60 changes: 52 additions & 8 deletions storage/mysql/68-mysql.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,41 +53,85 @@
<label for="node-input-mydb"><i class="fa fa-database"></i> Database</label>
<input type="text" id="node-input-mydb">
</div>
<div class="form-row">
<label for="node-input-parameterSource"><i class="fa fa-edit"></i> Input Source </label>
<input type="text" id="node-input-parameterSource" placeholder="payload" style="width:250px;">
<input type="hidden" id="node-input-fieldType">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row" style="position: relative; margin-bottom: 0px;">
<label for="node-input-query"><i class="fa fa-file-code-o"></i> Query</label>
<input type="hidden" id="node-input-query" autofocus="autofocus">
</div>
<div class="form-row node-text-editor-row">
<div style="min-height:150px;" class="node-text-editor" id="node-input-mysql-query-editor" ></div>
</div>
</script>

<script type="text/x-red" data-help-name="mysql">
<p>Allows basic access to a MySQL database.</p>
<p>This node uses the <b>query</b> operation against the configured database. This does allow both INSERTS and DELETES.
By its very nature it allows SQL injection... so <i>be careful out there...</i></p>
<p><code>msg.topic</code> must hold the <i>query</i> for the database, and the result is returned in <code>msg.payload</code>.</p>
<p><code>msg.payload</code> can contain an array of values to bind to the topic.</p>
<p>This node uses the <b>query</b> operation against the configured database. This does allow both INSERTS and DELETES.</i></p>
<p>Using legacy method of queries set in `msg.topic` allows SQL injection... so <i>be careful out there...</i>
<h3>Query</h3>
<p>Enter SQL queries and escapes input values that are located at a set path in the <code>msg</code> object.</p>
<p>SQL queries can use mustache style variable insertion. If our <code>msg.payload</code> has a property <code>key</code>, we would write a query as following:</p>
<pre>
SELECT *
FROM table
WHERE column = {{key}};
</pre>
<p>For escaped input clarification, you can refer to the documentation for <a href="https://github.com/mysqljs/mysql#escaping-query-values">mysqljs/mysql</a>.
<h3>Results</h3>
<p>Typically the returned payload will be an array of the result rows.</p>
<p>If nothing is found for the key then <i>null</i> is returned,</p>
<h3>Misc.</h3>
<p>The reconnect timeout in milliseconds can be changed by adding a line to <b>settings.js</b>
<pre>mysqlReconnectTime: 30000,</pre></p>
<h3>Legacy</h3>
<p><code>msg.topic</code> must hold the <i>query</i> for the database, and the result is returned in <code>msg.payload</code>.</p>
<p><code>msg.payload</code> can contain an array of values to bind to the topic.</p>
</script>

<script type="text/javascript">
RED.nodes.registerType('mysql',{
category: 'storage-input',
color:"#e97b00",
color:"rgb(233, 143, 20)",
defaults: {
mydb: {type:"MySQLdatabase",required:true},
name: {value:""}
name: {value:""},
parameterSource: {value: 'payload' },
fieldType: {value:"msg"},
query: {value: ""}
},
inputs:1,
outputs:1,
icon: "db.png",
icon: "sql.png",
label: function() {
var levelNode = RED.nodes.node(this.mydb);
return this.name||(levelNode?levelNode.label():"mysql");
},
labelStyle: function() {
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function(){
$("#node-input-parameterSource").typedInput({
default: 'msg',
types: ['msg'],
typeField: $("#node-input-fieldType")
});
this.editor = RED.editor.createEditor({
id: 'node-input-mysql-query-editor',
mode: 'ace/mode/sql',
value: $("#node-input-query").val()
});

},
oneditsave: function() {
$("#node-input-query").val(this.editor.getValue())
delete this.editor;
}
});
</script>
77 changes: 61 additions & 16 deletions storage/mysql/68-mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
35 changes: 29 additions & 6 deletions storage/mysql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,38 @@ Usage

Allows basic access to a MySQL database.

This node uses the <b>query</b> 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 <i>be careful out there...</i>
Using legacy method where queries are set in `msg.topic` allows SQL injection... so be careful out there...

The `msg.topic` must hold the <i>query</i> 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 <i>null</i> 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 <b>settings.js</b>
<pre>mysqlReconnectTime: 30000,</pre></p>
`msg.payload` can contain an array of values to bind to the topic.
Binary file added storage/mysql/icons/sql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions storage/mysql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,21 @@
"name": "Dave Conway-Jones",
"email": "[email protected]",
"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"
}
}
129 changes: 129 additions & 0 deletions test/storage/mysql/mysql_spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});