Compare commits

...

20 Commits

Author SHA1 Message Date
Dave Conway-Jones
63cf6dd6df WIP re quotes 2022-10-29 16:53:14 +01:00
Dave Conway-Jones
501c78666d Fix CSV node to handle headers with quotes and spaces
This is a breaking change so needs thinking about
Includes updated tests
2022-10-19 09:19:54 +01:00
Dave Conway-Jones
5b27bcd781 CSV node check header properties for ' and "
and add test
to close #3919
2022-10-16 18:05:21 +01:00
Nick O'Leary
32999ffa84 Merge pull request #3906 from node-red/Fix-for-csv-undefined-property
Fix for CSV undefined property
2022-10-04 15:38:37 +01:00
Nick O'Leary
f06c53f1f1 Merge pull request #3905 from node-red/mqtt-followups
Fix birth topic handling in MQTT node
2022-10-04 15:36:49 +01:00
Nick O'Leary
a9eec28360 Merge pull request #3884 from node-red/fix-auto-complete
Fix autocomplete entry for responseUrl
2022-10-04 15:35:10 +01:00
Nick O'Leary
5cda972872 Merge pull request #3890 from kazuhitoyokoi/master-fixmqttproperty
Fix pull-down menus of MQTT configuration node
2022-10-04 15:34:50 +01:00
Nick O'Leary
087946876b Merge pull request #3907 from boahc077/github_actions_token_permission
ci: add minimum GitHub token permissions for workflows
2022-10-04 15:32:47 +01:00
Nick O'Leary
318f0f1b7e Merge pull request #3899 from node-red/fix-change-self-overwrite
Fix change node overwriting msg with itself
2022-10-04 15:29:23 +01:00
Ashish Kurmi
87e7f3a61c ci: add minimum GitHub token permissions for workflows
Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>
2022-10-02 11:16:13 -07:00
Dave Conway-Jones
e724f216bf Fix for CSV undefined property
to close #3900 main issue
and add tests
(other fix is commented out but no tests)
2022-09-30 13:48:48 +01:00
Steve-Mcl
b0abba15a6 remove dud code instead of commenting 2022-09-29 19:08:46 +01:00
Steve-Mcl
81b4874a7c fix new test and fix bug found in previous PR 2022-09-29 19:05:53 +01:00
Steve-Mcl
f11b9c1e18 add test bad birth topic
part of #3865
2022-09-29 13:12:15 +01:00
Steve-Mcl
e15ecc00ce remove old unused code (5y+ not used) 2022-09-29 13:11:25 +01:00
Dave Conway-Jones
3e4c45ac6a Fix change node overwriting msg with itself
and add test
to close #3891
2022-09-22 20:22:11 +01:00
Kazuhito Yokoi
f872e2ab80 Add icon to typedInput in MQTT node 2022-09-18 19:11:33 +09:00
Kazuhito Yokoi
a81b1aa0cb Support i18n in MQTT node property 2022-09-18 17:10:19 +09:00
Kazuhito Yokoi
efc0f1ab91 Fix default values for MQTT retain settings 2022-09-18 16:24:25 +09:00
Nick O'Leary
9fd4989142 Fix autocomplete entry for responseUrl
Fixes #3883
2022-09-15 21:22:55 +01:00
12 changed files with 203 additions and 58 deletions

View File

@@ -5,6 +5,9 @@ on:
release:
types: [published]
permissions:
contents: read
jobs:
generate:
name: 'Update node-red-docker image'

View File

@@ -6,8 +6,14 @@ on:
pull_request:
branches: [ master, dev ]
permissions:
contents: read
jobs:
build:
permissions:
checks: write # for coverallsapp/github-action to create new checks
contents: read # for actions/checkout to fetch code
runs-on: ubuntu-latest
strategy:
matrix:

View File

@@ -146,7 +146,7 @@
{ value: "reset", source: ["delay","trigger","join","rbe"] },
{ value: "responseCookies", source: ["http request"] },
{ value: "responseTopic", source: ["mqtt"] },
{ value: "responseURL", source: ["http request"] },
{ value: "responseUrl", source: ["http request"] },
{ value: "restartTimeout", source: ["join"] },
{ value: "retain", source: ["mqtt"] },
{ value: "schema", source: ["json"] },

View File

@@ -318,7 +318,7 @@ module.exports = function(RED) {
}
var r = node.rules[currentRule];
if (r.t === "move") {
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1)) {
if ((r.tot !== r.pt) || (r.p.indexOf(r.to) !== -1) && (r.p !== r.to)) {
applyRule(msg,{t:"set", p:r.to, pt:r.tot, to:r.p, tot:r.pt},(err,msg) => {
applyRule(msg,{t:"delete", p:r.p, pt:r.pt}, (err,msg) => {
completeApplyingRules(msg,currentRule,done);

View File

@@ -421,7 +421,11 @@
<script type="text/javascript">
(function() {
var typedInputNoneOpt = { value: 'none', label: '', hasValue: false };
var typedInputNoneOpt = {
value: 'none',
label: RED._("node-red:mqtt.label.none"),
hasValue: false
};
var makeTypedInputOpt = function(value){
return {
value: value,
@@ -436,7 +440,11 @@
makeTypedInputOpt("text/csv"),
makeTypedInputOpt("text/html"),
makeTypedInputOpt("text/plain"),
{value:"other", label:""}
{
value: "other",
label: RED._("node-red:mqtt.label.other"),
icon: "red/images/typedInput/az.svg"
}
];
function getDefaultContentType(value) {
@@ -499,17 +507,17 @@
cleansession: {value: true},
birthTopic: {value:"", validate:validateMQTTPublishTopic},
birthQos: {value:"0"},
birthRetain: {value:false},
birthRetain: {value:"false"},
birthPayload: {value:""},
birthMsg: { value: {}},
closeTopic: {value:"", validate:validateMQTTPublishTopic},
closeQos: {value:"0"},
closeRetain: {value:false},
closeRetain: {value:"false"},
closePayload: {value:""},
closeMsg: { value: {}},
willTopic: {value:"", validate:validateMQTTPublishTopic},
willQos: {value:"0"},
willRetain: {value:false},
willRetain: {value:"false"},
willPayload: {value:""},
willMsg: { value: {}},
userProps: { value: ""},

View File

@@ -459,7 +459,6 @@ module.exports = function(RED) {
if(!opts || typeof opts !== "object") {
return; //nothing to change, simply return
}
const originalBrokerURL = node.brokerurl;
//apply property changes (only if the property exists in the opts object)
setIfHasProperty(opts, node, "url", init);
@@ -468,7 +467,6 @@ module.exports = function(RED) {
setIfHasProperty(opts, node, "clientid", init);
setIfHasProperty(opts, node, "autoConnect", init);
setIfHasProperty(opts, node, "usetls", init);
setIfHasProperty(opts, node, "usews", init);
setIfHasProperty(opts, node, "verifyservercert", init);
setIfHasProperty(opts, node, "compatmode", init);
setIfHasProperty(opts, node, "protocolVersion", init);
@@ -571,9 +569,6 @@ module.exports = function(RED) {
if (typeof node.usetls === 'undefined') {
node.usetls = false;
}
if (typeof node.usews === 'undefined') {
node.usews = false;
}
if (typeof node.verifyservercert === 'undefined') {
node.verifyservercert = false;
}
@@ -1000,7 +995,7 @@ module.exports = function(RED) {
node.client.publish(msg.topic, msg.payload, options, function (err) {
if (done) {
done(err)
} else {
} else if(err) {
node.error(err, msg)
}
})

View File

@@ -38,16 +38,38 @@ module.exports = function(RED) {
if (this.hdrout === true) { this.hdrout = "all"; }
var tmpwarn = true;
var node = this;
var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g');
// pass in an array of column names to be trimmed, de-quoted and retrimmed
var clean = function(col,sep) {
if (sep) { re = new RegExp(sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') +'(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); }
col = col.trim().split(re) || [""];
col = col.map(x => x.replace(/"/g,'').trim());
if ((col.length === 1) && (col[0] === "")) { node.goodtmpl = false; }
var ff = true; // flag to indicate if inside or outside a pair of quotes true = outside.
var jj = 0; // pointer into array of template items
var kk = [""]; // array of data for each of the template items
for (var ii = 0; ii < col.length; ii++) {
//console.log("II",ii,col[ii]);
if (col[ii] === node.quo) { // if it's a quote toggle inside or outside
if (ii === 0 || col[ii-1] === sep) { ff = !ff; }
else if (col[ii] === node.quo && col[ii+1] === node.quo) { kk[jj] += '\\'; } // do nothing, "" = " in CSV world
else if (!ff && kk[jj][0] !== node.quo) { ff = !ff; }
else { kk[jj] += col[ii]; }
}
else if ((col[ii] === sep) && ff) { // if it is the end of the group then finish
jj += 1;
ff = true;
kk[jj] = col.length - 1 === ii ? null : "";
}
else if (col[ii] === " " && ff && (ii == 0 | col[ii-1] == sep | col[ii+1] == sep)) {
// skip
}
else {
kk[jj] += col[ii];
}
}
if ((kk.length === 1) && (kk[0] === "")) { node.goodtmpl = false; }
else { node.goodtmpl = true; }
return col;
console.log("KK",kk)
return kk;
}
var template = clean(node.template,',');
var notemplate = template.length === 1 && template[0] === '';
@@ -61,7 +83,7 @@ module.exports = function(RED) {
if (typeof msg.payload == "object") { // convert object to CSV string
try {
if (!(notemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) {
template = clean(node.template);
template = clean(node.template,",");
}
var ou = "";
if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; }
@@ -74,7 +96,18 @@ module.exports = function(RED) {
template = Object.keys(msg.payload[0]);
}
}
ou += template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep) + node.ret;
var t = RED.util.cloneMessage(template);
template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v)
for (var i=0; i < t.length; i++) {
var v = t[i];
if (v.indexOf('"') !== -1 ) { v = v.replaceAll('"','""'); }
if (v.indexOf(node.sep)!==-1 || v.indexOf('"')!==-1 ) { v = '"'+v+'"'; }
console.log("V3",v)
t[i]=v;
}
console.log("T",t)
console.log("Te",template)
ou += t.join(node.sep) + node.ret;
if (node.hdrout === "once") { node.hdrSent = true; }
}
for (var s = 0; s < msg.payload.length; s++) {
@@ -110,7 +143,12 @@ module.exports = function(RED) {
if (msg.payload[s].hasOwnProperty(p)) {
/* istanbul ignore else */
if (typeof msg.payload[s][p] !== "object") {
var q = "" + msg.payload[s][p];
// Fix to honour include null values flag
//if (typeof msg.payload[s][p] !== "object" || (node.include_null_values === true && msg.payload[s][p] === null)) {
var q = "";
if (msg.payload[s][p] !== undefined) {
q += msg.payload[s][p];
}
if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes
q = q.replace(/"/g, '""');
ou += node.quo + q + node.quo + node.sep;
@@ -130,9 +168,18 @@ module.exports = function(RED) {
ou += node.sep;
}
else {
var p = RED.util.ensureString(RED.util.getMessageProperty(msg,"payload["+s+"]['"+template[t]+"']"));
var tt = template[t];
if (template[t].indexOf('"') >=0 ) {
tt = tt.replaceAll("'","\\'");
tt = "'"+tt+"'";
}
else { tt = '"'+tt+'"'; }
var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']');
/* istanbul ignore else */
if (p === "undefined") { p = ""; }
if (p === undefined) { p = ""; }
// fix to honour include null values flag
//if (p === null && node.include_null_values !== true) { p = "";}
p = RED.util.ensureString(p);
if (p.indexOf(node.quo) !== -1) { // add double quotes if any quotes
p = p.replace(/"/g, '""');
ou += node.quo + p + node.quo + node.sep;
@@ -188,7 +235,7 @@ module.exports = function(RED) {
if ((node.hdrin === true) && first) { // if the template is in the first line
if ((line[i] === "\n")||(line[i] === "\r")||(line.length - i === 1)) { // look for first line break
if (line.length - i === 1) { tmp += line[i]; }
template = clean(tmp,node.sep);
template = clean(tmp.trimEnd(),node.sep);
first = false;
}
else { tmp += line[i]; }

View File

@@ -446,7 +446,9 @@
"staticTopic": "Subscribe to single topic",
"dynamicTopic": "Dynamic subscription",
"auto-connect": "Connect automatically",
"auto-mode-depreciated": "This option is depreciated. Please use the new auto-detect mode."
"auto-mode-depreciated": "This option is depreciated. Please use the new auto-detect mode.",
"none": "none",
"other": "other"
},
"sections-label": {
"birth-message": "Message sent on connection (birth message)",

View File

@@ -446,7 +446,9 @@
"staticTopic": "1つのトピックを購読",
"dynamicTopic": "動的な購読",
"auto-connect": "自動接続",
"auto-mode-depreciated": "本オプションは非推奨になりました。新しい自動判定モードを使用してください。"
"auto-mode-depreciated": "本オプションは非推奨になりました。新しい自動判定モードを使用してください。",
"none": "なし",
"other": "その他"
},
"sections-label": {
"birth-message": "接続時の送信メッセージ(Birthメッセージ)",

View File

@@ -1717,6 +1717,24 @@ describe('change Node', function() {
changeNode1.receive({topic:{foo:{bar:1}}, payload:"String"});
});
});
it('moves the value of a message property object to itself', function(done) {
var flow = [{"id":"changeNode1","type":"change","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload","tot":"msg"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
helperNode1.on("input", function(msg) {
try {
msg.should.have.property('payload');
msg.payload.should.equal("bar");
done();
} catch(err) {
done(err);
}
});
changeNode1.receive({payload:"bar"});
});
});
it('moves the value of a message property object to a sub-property', function(done) {
var flow = [{"id":"changeNode1","type":"change","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.foo","tot":"msg"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];

View File

@@ -489,6 +489,39 @@ describe('MQTT Nodes', function () {
}
testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks);
});
itConditional('should safely discard bad birth topic', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const baseTopic = nextTopic();
const brokerOptions = {
protocolVersion: 4,
birthTopic: baseTopic + "#", // a publish topic should never have a wildcard
birthPayload: "broker connected",
birthQos: 2,
}
const options = {};
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null };
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
helperNode.on("input", function (msg) {
try {
msg.should.have.a.property("error").type("object");
msg.error.should.have.a.property("source").type("object");
msg.error.source.should.have.a.property("id", mqttIn.id);
done();
} catch (err) {
done(err)
}
});
return true; //handled
}
options.expectMsg = null;
try {
testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks);
done()
} catch(err) {
done(e)
}
});
itConditional('should publish close message', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
@@ -646,12 +679,13 @@ function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, options, hoo
const mqttBroker = helper.getNode(brokerOptions.id);
const mqttIn = helper.getNode(nodes.mqtt_in.id);
const mqttOut = helper.getNode(nodes.mqtt_out.id);
let afterLoadHandled = false;
let afterLoadHandled = false, finished = false;
if (hooks.afterLoad) {
afterLoadHandled = hooks.afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)
}
if (!afterLoadHandled) {
helperNode.on("input", function (msg) {
finished = true
try {
compareMsgToExpected(msg, expectMsg);
if (hooks.done) { hooks.done(); }
@@ -676,10 +710,12 @@ function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, options, hoo
}
})
.catch((e) => {
if(finished) { return }
if (hooks.done) { hooks.done(e); }
else { throw e; }
});
} catch (err) {
if(finished) { return }
if (hooks.done) { hooks.done(err); }
else { throw err; }
}

View File

@@ -138,21 +138,22 @@ describe('CSV node', function() {
});
});
it('should remove quotes and whitespace from template', function(done) {
var flow = [ { id:"n1", type:"csv", temp:'"a", "b" , " c "," d " ', wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
check_parts(msg, 0, 1);
done();
});
var testString = "1,2,3,4"+String.fromCharCode(10);
n1.emit("input", {payload:testString});
});
});
// it('should remove quotes and whitespace from template', function(done) {
// var flow = [ { id:"n1", type:"csv", temp:'"a", "b" , " c "," d " ', wires:[["n2"]] },
// {id:"n2", type:"helper"} ];
// helper.load(csvNode, flow, function() {
// var n1 = helper.getNode("n1");
// var n2 = helper.getNode("n2");
// n2.on("input", function(msg) {
// console.log("GOT",msg.payload)
// msg.should.have.property('payload', { a: 1, b: 2, " c ": 3, " d ": 4 });
// check_parts(msg, 0, 1);
// done();
// });
// var testString = "1,2,3,4"+String.fromCharCode(10);
// n1.emit("input", {payload:testString});
// });
// });
it('should create column names if no template provided', function(done) {
var flow = [ { id:"n1", type:"csv", temp:'', wires:[["n2"]] },
@@ -195,8 +196,8 @@ describe('CSV node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c,c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,"c,c","d, d"');
msg.should.have.property('payload', { a: 1, "b b":2, "c,c":3, " d, d ": 4 });
msg.should.have.property('columns', 'a,b b,"c,c"," d, d "');
check_parts(msg, 0, 1);
done();
});
@@ -212,8 +213,8 @@ describe('CSV node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c,c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,"c,c","d, d"');
msg.should.have.property('payload', { a: 1, "b b":2, "c,c":3, " d, d ": 4 });
msg.should.have.property('columns', 'a,b b,"c,c"," d, d "');
check_parts(msg, 0, 1);
done();
});
@@ -229,8 +230,8 @@ describe('CSV node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c;c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c;c,"d, d"');
msg.should.have.property('payload', { a: 1, "b b":2, "c;c":3, " d, d ": 4 });
msg.should.have.property('columns', 'a,b b,c;c," d, d "');
check_parts(msg, 0, 1);
done();
});
@@ -246,8 +247,8 @@ describe('CSV node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c/c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c/c,"d, d"');
msg.should.have.property('payload', { a: 1, "b b":2, "c/c":3, " d, d ": 4 });
msg.should.have.property('columns', 'a,b b,c/c," d, d "');
check_parts(msg, 0, 1);
done();
});
@@ -263,8 +264,8 @@ describe('CSV node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property('payload', { a: 1, "b b":2, "c\\c":3, "d, d": 4 });
msg.should.have.property('columns', 'a,b b,c\\c,"d, d"');
msg.should.have.property('payload', { a: 1, "b b":2, "c\\c":3, " d, d ": 4 });
msg.should.have.property('columns', 'a,b b,c\\c," d, d "');
check_parts(msg, 0, 1);
done();
});
@@ -693,19 +694,19 @@ describe('CSV node', function() {
describe('json object to csv', function() {
it('should convert a simple object back to a csv', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"a,b,c,,e,f", wires:[["n2"]] },
var flow = [ { id:"n1", type:"csv", temp:"a,b,c,,e,f,g,h,i,j,k", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '4,foo,true,,0,"Hello\nWorld"\n');
msg.should.have.property('payload', '4,foo,true,,0,"Hello\nWorld",,,undefined,null,null\n');
done();
}
catch(e) { done(e); }
});
var testJson = { e:0, d:1, b:"foo", c:true, a:4, f:"Hello\nWorld" };
var testJson = { e:0, d:1, b:"foo", c:true, a:4, f:"Hello\nWorld", h:undefined, i:"undefined",j:null,k:"null" };
n1.emit("input", {payload:testJson});
});
});
@@ -718,12 +719,12 @@ describe('CSV node', function() {
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '1,foo,"ba""r","di,ng"\n');
msg.should.have.property('payload', '1,foo,"ba""r","di,ng",,undefined,null\n');
done();
}
catch(e) { done(e); }
});
var testJson = { d:1, b:"foo", c:"ba\"r", a:"di,ng" };
var testJson = { d:1, b:"foo", c:"ba\"r", a:"di,ng", e:undefined, f:"undefined", g:null,h:"null" };
n1.emit("input", {payload:testJson});
});
});
@@ -764,6 +765,33 @@ describe('CSV node', function() {
});
});
it('should handle a template with quotes in the property names', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"", hdrout:"all", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(csvNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n');
done();
}
catch(e) { done(e); }
});
var testJson = [
{
"a\"a": "A1",
"b'b": "B1"
},
{
"a\"a": "A2",
"b'b": "B2"
}
]
n1.emit("input", {payload:testJson});
});
});
it('should convert an array of objects to a multi-line csv', function(done) {
var flow = [ { id:"n1", type:"csv", temp:"a,d,c,b", wires:[["n2"]] },
{id:"n2", type:"helper"} ];