diff --git a/CHANGELOG.md b/CHANGELOG.md index 618c3d8b5..68ea56f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +#### 3.1.7: Maintenance Release + + - Add Japanese translation for v3.1.6 (#4603) @kazuhitoyokoi + - Update jsonata version (#4593) @hardillb + +#### 3.1.6: Maintenance Release + +Editor + + - Do not flag env var in num typedInput as error (#4582) @knolleary + - Fix example flow name in import dialog (#4578) @kazuhitoyokoi + - Fix missing node icons in workspace (#4570) @knolleary + +Runtime + + - Handle undefined env vars (#4581) @knolleary + - fix: Removed offending MD5 crypto hash and replaced with SHA1 and SHA256 … (#4568) @JaysonHurst + - chore: remove never use import code (#4580) @giscafer + +Nodes + + - fix: template node zh-CN translation (#4575) @giscafer + #### 3.1.5: Maintenance Release Runtime diff --git a/package.json b/package.json index 9943d3203..173e4c929 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "is-utf8": "0.2.1", "js-yaml": "4.1.0", "json-stringify-safe": "5.0.1", - "jsonata": "1.8.6", + "jsonata": "2.0.4", "lodash.clonedeep": "^4.5.0", "media-typer": "1.1.0", "memorystore": "1.6.7", diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/settings.js b/packages/node_modules/@node-red/editor-api/lib/admin/settings.js index d72f9e094..425d42415 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/settings.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/settings.js @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var apiUtils = require("../util"); var runtimeAPI; var settings; var theme = require("../editor/theme"); diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js b/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js index e18925c19..b071a9caf 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/strategies.js @@ -18,7 +18,6 @@ var BearerStrategy = require('passport-http-bearer').Strategy; var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy; var passport = require("passport"); -var crypto = require("crypto"); var util = require("util"); var Tokens = require("./tokens"); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index 648daa09b..54cf17f12 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -14,11 +14,9 @@ * limitations under the License. **/ -var express = require("express"); var path = require('path'); var comms = require("./comms"); -var library = require("./library"); var info = require("./settings"); var auth = require("../auth"); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/library.js b/packages/node_modules/@node-red/editor-api/lib/editor/library.js index cd564b3f8..7853227ea 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/library.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/library.js @@ -15,8 +15,6 @@ **/ var apiUtils = require("../util"); -var fs = require('fs'); -var fspath = require('path'); var runtimeAPI; diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js index 568f5837b..6109d8741 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/locales.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/locales.js @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var fs = require('fs'); -var path = require('path'); -// var apiUtil = require('../util'); var i18n = require("@node-red/util").i18n; // TODO: separate module diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js index 08097571f..885967b91 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js @@ -15,7 +15,6 @@ **/ var apiUtils = require("../util"); -var express = require("express"); var runtimeAPI; var settings; diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index e5c3904c7..c3808a751 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -14,7 +14,6 @@ * limitations under the License. **/ -var express = require("express"); var util = require("util"); var path = require("path"); var fs = require("fs"); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js index e7bf15069..37c79d415 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js @@ -99,7 +99,7 @@ module.exports = { // settings.instanceId is set asynchronously to the editor-api // being initiaised. So we defer calculating the cacheBuster hash // until the first load of the editor - cacheBuster = crypto.createHash('md5').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12) + cacheBuster = crypto.createHash('sha1').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12) } let sessionMessages; diff --git a/packages/node_modules/@node-red/editor-api/lib/index.js b/packages/node_modules/@node-red/editor-api/lib/index.js index d9f34eafd..9264550b3 100644 --- a/packages/node_modules/@node-red/editor-api/lib/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/index.js @@ -24,11 +24,8 @@ * @namespace @node-red/editor-api */ -var express = require("express"); var bodyParser = require("body-parser"); -var util = require('util'); var passport = require('passport'); -var cors = require('cors'); var auth = require("./auth"); var apiUtil = require("./util"); diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index efd4e04ab..da9bf18a2 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -926,6 +926,12 @@ "env": "env variable", "cred": "credential", "conf-types": "config node" + }, + "date": { + "format": { + "timestamp": "milliseconds since epoch", + "object": "JavaScript Date Object" + } } }, "editableList": { diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 5d988c68a..53c8616fd 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -303,7 +303,8 @@ "missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません" }, "conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。", - "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。" + "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。", + "alreadyExists": "本ノードは既に存在" }, "copyMessagePath": "パスをコピーしました", "copyMessageValue": "値をコピーしました", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 16c4cdf8e..165e9cce8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -408,7 +408,25 @@ } }, re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"}, - date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false}, + date: { + value:"date", + label:"timestamp", + icon:"fa fa-clock-o", + options:[ + { + label: 'milliseconds since epoch', + value: '' + }, + { + label: 'YYYY-MM-DDTHH:mm:ss.sssZ', + value: 'iso' + }, + { + label: 'JavaScript Date Object', + value: 'object' + } + ] + }, jsonata: { value: "jsonata", label: "expression", @@ -709,6 +727,10 @@ allOptions.flow.options = contextStoreOptions; allOptions.global.options = contextStoreOptions; } + // Translate timestamp options + allOptions.date.options.forEach(opt => { + opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label}) + }) } nlsd = true; var that = this; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 6475b19f5..94bf19718 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -483,6 +483,16 @@ RED.utils = (function() { $('').css('backgroundColor',obj).appendTo(e); } + let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj); + if (n) { + if (options.nodeSelector && "function" == typeof options.nodeSelector) { + e.css('cursor', 'pointer').on("click", function(evt) { + evt.preventDefault(); + options.nodeSelector(n.id); + }) + } + } + } else if (typeof obj === 'number') { e = $('').appendTo(entryObj); @@ -589,6 +599,7 @@ RED.utils = (function() { exposeApi: exposeApi, // tools: tools // Do not pass tools down as we // keep them attached to the top-level header + nodeSelector: options.nodeSelector, } ).appendTo(row); } @@ -619,6 +630,7 @@ RED.utils = (function() { exposeApi: exposeApi, // tools: tools // Do not pass tools down as we // keep them attached to the top-level header + nodeSelector: options.nodeSelector, } ).appendTo(row); } @@ -675,6 +687,7 @@ RED.utils = (function() { exposeApi: exposeApi, // tools: tools // Do not pass tools down as we // keep them attached to the top-level header + nodeSelector: options.nodeSelector, } ).appendTo(row); } @@ -906,7 +919,10 @@ RED.utils = (function() { * @returns true if valid, String if invalid */ function validateTypedProperty(propertyValue, propertyType, opt) { - + if (propertyValue && /^\${[^}]+}$/.test(propertyValue)) { + // Allow ${ENV_VAR} value + return true + } let error if (propertyType === 'json') { try { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 23385940c..b3ed6a6cc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4156,7 +4156,7 @@ RED.view = (function() { } var width = img.width * scaleFactor; if (width > 20) { - scalefactor *= 20/width; + scaleFactor *= 20/width; width = 20; } var height = img.height * scaleFactor; diff --git a/packages/node_modules/@node-red/editor-client/src/js/validators.js b/packages/node_modules/@node-red/editor-client/src/js/validators.js index 1673495aa..c17955ce1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/validators.js +++ b/packages/node_modules/@node-red/editor-client/src/js/validators.js @@ -16,8 +16,20 @@ RED.validators = { number: function(blankAllowed,mopt){ return function(v, opt) { - if ((blankAllowed&&(v===''||v===undefined)) || (v!=='' && !isNaN(v))) { - return true; + if (blankAllowed && (v === '' || v === undefined)) { + return true + } + if (v !== '') { + if (/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(v)) { + return true + } + if (/^\${[^}]+}$/.test(v)) { + // Allow ${ENV_VAR} value + return true + } + } + if (!isNaN(v)) { + return true } if (opt && opt.label) { return RED._("validator.errors.invalid-num-prop", { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss index 92a98913f..58e77863d 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss @@ -38,7 +38,7 @@ body { } #red-ui-main-container { position: absolute; - top:40px; left:0; bottom: 0; right:0; + top: var(--red-ui-header-height); left:0; bottom: 0; right:0; overflow:hidden; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index ce71bcdba..b561fde16 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -259,7 +259,8 @@ $deploy-button-background-disabled-hover: #555; $header-background: #000; $header-button-background-active: #121212; -$header-menu-color: #C7C7C7; +$header-accent: #d41313; +$header-menu-color: #eee; $header-menu-color-disabled: #666; $header-menu-heading-color: #fff; $header-menu-sublabel-color: #aeaeae; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/header.scss b/packages/node_modules/@node-red/editor-client/src/sass/header.scss index 723c1e9bd..a80ad3a17 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/header.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/header.scss @@ -23,16 +23,20 @@ top: 0; left: 0; width: 100%; - height: 40px; + height: var(--red-ui-header-height); background: var(--red-ui-header-background); box-sizing: border-box; padding: 0px 0px 0px 20px; color: var(--red-ui-header-menu-color); font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid var(--red-ui-header-accent); + padding-top: 2px; span.red-ui-header-logo { float: left; - margin-top: 5px; font-size: 30px; line-height: 30px; text-decoration: none; @@ -42,7 +46,7 @@ vertical-align: middle; font-size: 16px !important; &:not(:first-child) { - margin-left: 5px; + margin-left: 8px; } } img { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/sizes.scss b/packages/node_modules/@node-red/editor-client/src/sass/sizes.scss new file mode 100644 index 000000000..a3d48e76d --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/sass/sizes.scss @@ -0,0 +1,17 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + $header-height: 48px; \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style-custom-theme.scss b/packages/node_modules/@node-red/editor-client/src/sass/style-custom-theme.scss index 1202d9fb7..312081503 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style-custom-theme.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style-custom-theme.scss @@ -15,4 +15,5 @@ **/ @import "colors"; +@import "sizes"; @import "variables"; \ No newline at end of file diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style.scss b/packages/node_modules/@node-red/editor-client/src/sass/style.scss index 7910832ad..412290f78 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style.scss @@ -15,6 +15,7 @@ **/ @import "colors"; +@import "sizes"; @import "variables"; @import "mixins"; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss index 50e1c9310..c04c26ff9 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss @@ -16,6 +16,9 @@ --red-ui-shadow: #{$shadow}; + // Header Height + --red-ui-header-height: #{$header-height}; + // Main body text --red-ui-primary-text-color: #{$primary-text-color}; // UI control label text @@ -240,6 +243,7 @@ --red-ui-header-background: #{$header-background}; + --red-ui-header-accent: #{$header-accent}; --red-ui-header-button-background-active: #{$header-button-background-active}; --red-ui-header-menu-color: #{$header-menu-color}; --red-ui-header-menu-color-disabled: #{$header-menu-color-disabled}; diff --git a/packages/node_modules/@node-red/nodes/core/common/20-inject.html b/packages/node_modules/@node-red/nodes/core/common/20-inject.html index d50725d51..5895220d3 100644 --- a/packages/node_modules/@node-red/nodes/core/common/20-inject.html +++ b/packages/node_modules/@node-red/nodes/core/common/20-inject.html @@ -227,34 +227,42 @@ name: {value:""}, props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v, opt) { if (!v || v.length === 0) { return true } + const errors = [] for (var i=0;i 0) { + return errors + } return true; } }, repeat: { value:"", validate: function(v, opt) { if ((v === "") || - (RED.validators.number(v) && + (RED.validators.number()(v) && (v >= 0) && (v <= 2147483))) { return true; } @@ -263,7 +271,7 @@ }, crontab: {value:""}, once: {value:false}, - onceDelay: {value:0.1}, + onceDelay: {value:0.1, validate: RED.validators.number(true)}, topic: {value:""}, payload: {value:"", validate: RED.validators.typedInput("payloadType", false) }, payloadType: {value:"date"}, diff --git a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js index a4cd4c68d..b244273dc 100644 --- a/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js +++ b/packages/node_modules/@node-red/nodes/core/common/lib/debug/debug-utils.js @@ -512,7 +512,8 @@ RED.debug = (function() { hideKey: false, path: path, sourceId: sourceNode&&sourceNode.id, - rootPath: path + rootPath: path, + nodeSelector: config.messageSourceClick, }); // Do this in a separate step so the element functions aren't stripped debugMessage.appendTo(el); diff --git a/packages/node_modules/@node-red/nodes/core/function/15-change.js b/packages/node_modules/@node-red/nodes/core/function/15-change.js index 0bbd81955..64ce00b88 100644 --- a/packages/node_modules/@node-red/nodes/core/function/15-change.js +++ b/packages/node_modules/@node-red/nodes/core/function/15-change.js @@ -117,7 +117,7 @@ module.exports = function(RED) { }); return } else if (rule.tot === 'date') { - value = Date.now(); + value = RED.util.evaluateNodeProperty(rule.to, rule.tot, node) } else if (rule.tot === 'jsonata') { RED.util.evaluateJSONataExpression(rule.to,msg, (err, value) => { if (err) { diff --git a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js index 15401e374..500bbe2c2 100644 --- a/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js +++ b/packages/node_modules/@node-red/nodes/core/network/31-tcpin.js @@ -582,6 +582,7 @@ module.exports = function(RED) { const cc = Object.keys(clients).length; node.status({fill:"green",shape:cc===0?"ring":"dot",text:RED._("tcpin.status.connections",{count:cc})}); if ((host === undefined || port === undefined) && !msg.hasOwnProperty("payload")) { return; } + if (!msg.hasOwnProperty("payload")) { return; } } // Store client information independently diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.html index 46f416f9e..ddf306ba8 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.html +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.html @@ -17,7 +17,20 @@ - +
+ +
+ +
+
+
+
+
+
@@ -60,10 +73,10 @@
- + -
@@ -75,6 +88,7 @@ color:"#DEBD5C", defaults: { name: {value:""}, + spec: {value:"rfc"}, sep: { value:',', required:true, label:RED._("node-red:csv.label.separator"), @@ -83,7 +97,7 @@ hdrin: {value:""}, hdrout: {value:"none"}, multi: {value:"one",required:true}, - ret: {value:'\\n'}, + ret: {value:'\\r\\n'}, // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)") temp: {value:""}, skip: {value:"0"}, strings: {value:true}, @@ -123,6 +137,27 @@ $("#node-input-sep").hide(); } }); + + $("#csv-option-spec").on("change", function() { + if ($("#csv-option-spec").val() == "rfc") { + $(".form-tips.csv-lecacy-warning").hide(); + } else { + $(".form-tips.csv-lecacy-warning").show(); + } + }); + // new nodes will have `spec` set to "rfc" (default), but existing nodes will either not have + // a spec value or it will be empty - we need to maintain the legacy behaviour for existing + // flows but default to rfc for new nodes + let spec = !this.spec ? "" : "rfc" + $("#csv-option-spec").val(spec).trigger("change") + }, + oneditsave: function() { + const specFormVal = $("#csv-option-spec").val() || '' // empty === legacy + const spectNodeVal = this.spec || '' // empty === legacy, null/undefined means in-place node upgrade (keep as is) + if (specFormVal !== spectNodeVal) { + // only update the flow value if changed (avoid marking the node dirty unnecessarily) + this.spec = specFormVal + } } }); diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js index 17bf56322..c6a91d61a 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-CSV.js @@ -15,322 +15,674 @@ **/ module.exports = function(RED) { + const csv = require('./lib/csv') + "use strict"; function CSVNode(n) { - RED.nodes.createNode(this,n); - this.template = (n.temp || ""); - this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r"); - this.quo = '"'; - this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r"); - this.winflag = (this.ret === "\r\n"); - this.lineend = "\n"; - this.multi = n.multi || "one"; - this.hdrin = n.hdrin || false; - this.hdrout = n.hdrout || "none"; - this.goodtmpl = true; - this.skip = parseInt(n.skip || 0); - this.store = []; - this.parsestrings = n.strings; - this.include_empty_strings = n.include_empty_strings || false; - this.include_null_values = n.include_null_values || false; - if (this.parsestrings === undefined) { this.parsestrings = true; } - if (this.hdrout === false) { this.hdrout = "none"; } - if (this.hdrout === true) { this.hdrout = "all"; } - var tmpwarn = true; - var node = this; - var re = new RegExp(node.sep.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g,'\\$&') + '(?=(?:(?:[^"]*"){2})*[^"]*$)','g'); + RED.nodes.createNode(this,n) + const node = this + const RFC4180Mode = n.spec === 'rfc' + const legacyMode = !RFC4180Mode - // 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; } - else { node.goodtmpl = true; } - return col; - } - var template = clean(node.template,','); - var notemplate = template.length === 1 && template[0] === ''; - node.hdrSent = false; + node.status({}) // clear status - this.on("input", function(msg, send, done) { - if (msg.hasOwnProperty("reset")) { - node.hdrSent = false; + if (legacyMode) { + this.template = (n.temp || ""); + this.sep = (n.sep || ',').replace(/\\t/g,"\t").replace(/\\n/g,"\n").replace(/\\r/g,"\r"); + this.quo = '"'; + this.ret = (n.ret || "\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r"); + this.winflag = (this.ret === "\r\n"); + this.lineend = "\n"; + this.multi = n.multi || "one"; + this.hdrin = n.hdrin || false; + this.hdrout = n.hdrout || "none"; + this.goodtmpl = true; + this.skip = parseInt(n.skip || 0); + this.store = []; + this.parsestrings = n.strings; + this.include_empty_strings = n.include_empty_strings || false; + this.include_null_values = n.include_null_values || false; + if (this.parsestrings === undefined) { this.parsestrings = true; } + if (this.hdrout === false) { this.hdrout = "none"; } + if (this.hdrout === true) { this.hdrout = "all"; } + var tmpwarn = true; + // var node = this; + 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; } + else { node.goodtmpl = true; } + return col; } - if (msg.hasOwnProperty("payload")) { - 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); - } - const ou = []; - if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; } - if (node.hdrout !== "none" && node.hdrSent === false) { - if ((template.length === 1) && (template[0] === '')) { - if (msg.hasOwnProperty("columns")) { - template = clean(msg.columns || "",","); - } - else { - template = Object.keys(msg.payload[0]); - } + var template = clean(node.template,','); + var notemplate = template.length === 1 && template[0] === ''; + node.hdrSent = false; + + this.on("input", function(msg, send, done) { + if (msg.hasOwnProperty("reset")) { + node.hdrSent = false; + } + if (msg.hasOwnProperty("payload")) { + 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); } - ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep)); - if (node.hdrout === "once") { node.hdrSent = true; } - } - for (var s = 0; s < msg.payload.length; s++) { - if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) { - if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; } - for (var t = 0; t < msg.payload[s].length; t++) { - if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; } - if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes - msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""'); - msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; - } - else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas" - msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; - } - else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n" - msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; - } - } - ou.push(msg.payload[s].join(node.sep)); - } - else { - if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) { - template = clean(msg.columns || "",","); - } + const ou = []; + if (!Array.isArray(msg.payload)) { msg.payload = [ msg.payload ]; } + if (node.hdrout !== "none" && node.hdrSent === false) { if ((template.length === 1) && (template[0] === '')) { - /* istanbul ignore else */ - if (tmpwarn === true) { // just warn about missing template once - node.warn(RED._("csv.errors.obj_csv")); - tmpwarn = false; + if (msg.hasOwnProperty("columns")) { + template = clean(msg.columns || "",","); } - const row = []; - for (var p in msg.payload[0]) { + else { + template = Object.keys(msg.payload[0]); + } + } + ou.push(template.map(v => v.indexOf(node.sep)!==-1 ? '"'+v+'"' : v).join(node.sep)); + if (node.hdrout === "once") { node.hdrSent = true; } + } + for (var s = 0; s < msg.payload.length; s++) { + if ((Array.isArray(msg.payload[s])) || (typeof msg.payload[s] !== "object")) { + if (typeof msg.payload[s] !== "object") { msg.payload = [ msg.payload ]; } + for (var t = 0; t < msg.payload[s].length; t++) { + if (msg.payload[s][t] === undefined) { msg.payload[s][t] = ""; } + if (msg.payload[s][t].toString().indexOf(node.quo) !== -1) { // add double quotes if any quotes + msg.payload[s][t] = msg.payload[s][t].toString().replace(/"/g, '""'); + msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; + } + else if (msg.payload[s][t].toString().indexOf(node.sep) !== -1) { // add quotes if any "commas" + msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; + } + else if (msg.payload[s][t].toString().indexOf("\n") !== -1) { // add quotes if any "\n" + msg.payload[s][t] = node.quo + msg.payload[s][t].toString() + node.quo; + } + } + ou.push(msg.payload[s].join(node.sep)); + } + else { + if ((template.length === 1) && (template[0] === '') && (msg.hasOwnProperty("columns"))) { + template = clean(msg.columns || "",","); + } + if ((template.length === 1) && (template[0] === '')) { /* istanbul ignore else */ - if (msg.payload[s].hasOwnProperty(p)) { + if (tmpwarn === true) { // just warn about missing template once + node.warn(RED._("csv.errors.obj_csv")); + tmpwarn = false; + } + const row = []; + for (var p in msg.payload[0]) { /* istanbul ignore else */ - if (typeof msg.payload[s][p] !== "object") { - // 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 (msg.payload[s].hasOwnProperty(p)) { + /* istanbul ignore else */ + if (typeof msg.payload[s][p] !== "object") { + // 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, '""'); + row.push(node.quo + q + node.quo); + } + else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n" + row.push(node.quo + q + node.quo); + } + else { row.push(q); } // otherwise just add } - if (q.indexOf(node.quo) !== -1) { // add double quotes if any quotes - q = q.replace(/"/g, '""'); - row.push(node.quo + q + node.quo); - } - else if (q.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n" - row.push(node.quo + q + node.quo); - } - else { row.push(q); } // otherwise just add } } + ou.push(row.join(node.sep)); // add separator } - ou.push(row.join(node.sep)); // add separator + else { + const row = []; + for (var t=0; t < template.length; t++) { + if (template[t] === '') { + row.push(''); + } + else { + var tt = template[t]; + if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; } + else { tt = '"'+tt+'"'; } + var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']'); + /* istanbul ignore else */ + 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, '""'); + row.push(node.quo + p + node.quo); + } + else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n" + row.push(node.quo + p + node.quo); + } + else { row.push(p); } // otherwise just add + } + } + ou.push(row.join(node.sep)); // add separator + } + } + } + // join lines, don't forget to add the last new line + msg.payload = ou.join(node.ret) + node.ret; + msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(','); + if (msg.payload !== '') { + send(msg); + } + done(); + } + catch(e) { done(e); } + } + else if (typeof msg.payload == "string") { // convert CSV string to object + try { + var f = true; // flag to indicate if inside or outside a pair of quotes true = outside. + var j = 0; // pointer into array of template items + var k = [""]; // array of data for each of the template items + var o = {}; // output object to build up + var a = []; // output array is needed for multiline option + var first = true; // is this the first line + var last = false; + var line = msg.payload; + var linecount = 0; + var tmp = ""; + var has_parts = msg.hasOwnProperty("parts"); + var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i; + if (msg.hasOwnProperty("parts")) { + linecount = msg.parts.index; + if (msg.parts.index > node.skip) { first = false; } + if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; } + } + + // For now we are just going to assume that any \r or \n means an end of line... + // got to be a weird csv that has singleton \r \n in it for another reason... + + // Now process the whole file/line + var nocr = (line.match(/[\r\n]/g)||[]).length; + if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; } + for (var i = 0; i < line.length; i++) { + if (first && (linecount < node.skip)) { + if (line[i] === "\n") { linecount += 1; } + continue; + } + 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); + first = false; + } + else { tmp += line[i]; } } else { - const row = []; - for (var t=0; t < template.length; t++) { - if (template[t] === '') { - row.push(''); + if (line[i] === node.quo) { // if it's a quote toggle inside or outside + f = !f; + if (line[i-1] === node.quo) { + if (f === false) { k[j] += '\"'; } + } // if it's a quotequote then it's actually a quote + //if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; } + } + else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish + if (!node.goodtmpl) { template[j] = "col"+(j+1); } + if ( template[j] && (template[j] !== "") ) { + // if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null + if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null; + if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } + if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; + if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; + if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; } - else { - var tt = template[t]; - if (template[t].indexOf('"') >=0 ) { tt = "'"+tt+"'"; } - else { tt = '"'+tt+'"'; } - var p = RED.util.getMessageProperty(msg,'payload["'+s+'"]['+tt+']'); - /* istanbul ignore else */ - 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, '""'); - row.push(node.quo + p + node.quo); - } - else if (p.indexOf(node.sep) !== -1 || p.indexOf("\n") !== -1) { // add quotes if any "commas" or "\n" - row.push(node.quo + p + node.quo); - } - else { row.push(p); } // otherwise just add + j += 1; + // if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",' + k[j] = line.length - 1 === i ? null : ""; + } + else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines + //console.log(j,k,o,k[j]); + if (!node.goodtmpl) { template[j] = "col"+(j+1); } + if ( template[j] && (template[j] !== "") ) { + // if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3' + if (line[i-1] === node.sep) k[j] = null; + if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } + else { if (k[j] !== null) k[j].replace(/\r$/,''); } + if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; + if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; + if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; } + if (JSON.stringify(o) !== "{}") { // don't send empty objects + a.push(o); // add to the array + } + j = 0; + k = [""]; + o = {}; + f = true; // reset in/out flag ready for next line. + } + else { // just add to the part of the message + k[j] += line[i]; } - ou.push(row.join(node.sep)); // add separator } } - } - // join lines, don't forget to add the last new line - msg.payload = ou.join(node.ret) + node.ret; - msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).join(','); - if (msg.payload !== '') { send(msg); } - done(); - } - catch(e) { done(e); } - } - else if (typeof msg.payload == "string") { // convert CSV string to object - try { - var f = true; // flag to indicate if inside or outside a pair of quotes true = outside. - var j = 0; // pointer into array of template items - var k = [""]; // array of data for each of the template items - var o = {}; // output object to build up - var a = []; // output array is needed for multiline option - var first = true; // is this the first line - var last = false; - var line = msg.payload; - var linecount = 0; - var tmp = ""; - var has_parts = msg.hasOwnProperty("parts"); - var reg = /^[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+$/i; - if (msg.hasOwnProperty("parts")) { - linecount = msg.parts.index; - if (msg.parts.index > node.skip) { first = false; } - if (msg.parts.hasOwnProperty("count") && (msg.parts.index+1 >= msg.parts.count)) { last = true; } - } + // Finished so finalize and send anything left + if (f === false) { node.warn(RED._("csv.errors.bad_csv")); } + if (!node.goodtmpl) { template[j] = "col"+(j+1); } - // For now we are just going to assume that any \r or \n means an end of line... - // got to be a weird csv that has singleton \r \n in it for another reason... - - // Now process the whole file/line - var nocr = (line.match(/[\r\n]/g)||[]).length; - if (has_parts && node.multi === "mult" && nocr > 1) { tmp = ""; first = true; } - for (var i = 0; i < line.length; i++) { - if (first && (linecount < node.skip)) { - if (line[i] === "\n") { linecount += 1; } - continue; + if ( template[j] && (template[j] !== "") ) { + if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } + else { if (k[j] !== null) k[j].replace(/\r$/,''); } + if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; + if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; + if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; } - 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); - first = false; - } - else { tmp += line[i]; } + + if (JSON.stringify(o) !== "{}") { // don't send empty objects + a.push(o); // add to the array } - else { - if (line[i] === node.quo) { // if it's a quote toggle inside or outside - f = !f; - if (line[i-1] === node.quo) { - if (f === false) { k[j] += '\"'; } - } // if it's a quotequote then it's actually a quote - //if ((line[i-1] !== node.sep) && (line[i+1] !== node.sep)) { k[j] += line[i]; } - } - else if ((line[i] === node.sep) && f) { // if it is the end of the line then finish - if (!node.goodtmpl) { template[j] = "col"+(j+1); } - if ( template[j] && (template[j] !== "") ) { - // if no value between separators ('1,,"3"...') or if the line beings with separator (',1,"2"...') treat value as null - if (line[i-1] === node.sep || line[i-1].includes('\n','\r')) k[j] = null; - if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } - if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; - if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; - if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; - } - j += 1; - // if separator is last char in processing string line (without end of line), add null value at the end - example: '1,2,3\n3,"3",' - k[j] = line.length - 1 === i ? null : ""; - } - else if (((line[i] === "\n") || (line[i] === "\r")) && f) { // handle multiple lines - //console.log(j,k,o,k[j]); - if (!node.goodtmpl) { template[j] = "col"+(j+1); } - if ( template[j] && (template[j] !== "") ) { - // if separator before end of line, set null value ie. '1,2,"3"\n1,2,\n1,2,3' - if (line[i-1] === node.sep) k[j] = null; - if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } - else { if (k[j] !== null) k[j].replace(/\r$/,''); } - if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; - if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; - if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; - } - if (JSON.stringify(o) !== "{}") { // don't send empty objects - a.push(o); // add to the array - } - j = 0; - k = [""]; - o = {}; - f = true; // reset in/out flag ready for next line. - } - else { // just add to the part of the message - k[j] += line[i]; - } - } - } - // Finished so finalize and send anything left - if (f === false) { node.warn(RED._("csv.errors.bad_csv")); } - if (!node.goodtmpl) { template[j] = "col"+(j+1); } - if ( template[j] && (template[j] !== "") ) { - if ( (k[j] !== null && node.parsestrings === true) && reg.test(k[j].trim()) ) { k[j] = parseFloat(k[j].trim()); } - else { if (k[j] !== null) k[j].replace(/\r$/,''); } - if (node.include_null_values && k[j] === null) o[template[j]] = k[j]; - if (node.include_empty_strings && k[j] === "") o[template[j]] = k[j]; - if (k[j] !== null && k[j] !== "") o[template[j]] = k[j]; - } - - if (JSON.stringify(o) !== "{}") { // don't send empty objects - a.push(o); // add to the array - } - - if (node.multi !== "one") { - msg.payload = a; - if (has_parts && nocr <= 1) { - if (JSON.stringify(o) !== "{}") { - node.store.push(o); + if (node.multi !== "one") { + msg.payload = a; + if (has_parts && nocr <= 1) { + if (JSON.stringify(o) !== "{}") { + node.store.push(o); + } + if (msg.parts.index + 1 === msg.parts.count) { + msg.payload = node.store; + msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); + delete msg.parts; + send(msg); + node.store = []; + } } - if (msg.parts.index + 1 === msg.parts.count) { - msg.payload = node.store; + else { msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); - delete msg.parts; - send(msg); - node.store = []; + send(msg); // finally send the array } } else { - msg.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); - send(msg); // finally send the array - } - } - else { - var len = a.length; - for (var i = 0; i < len; i++) { - var newMessage = RED.util.cloneMessage(msg); - newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); - newMessage.payload = a[i]; - if (!has_parts) { - newMessage.parts = { - id: msg._msgid, - index: i, - count: len - }; + var len = a.length; + for (var i = 0; i < len; i++) { + var newMessage = RED.util.cloneMessage(msg); + newMessage.columns = template.map(v => v.indexOf(',')!==-1 ? '"'+v+'"' : v).filter(v => v).join(','); + newMessage.payload = a[i]; + if (!has_parts) { + newMessage.parts = { + id: msg._msgid, + index: i, + count: len + }; + } + else { + newMessage.parts.index -= node.skip; + newMessage.parts.count -= node.skip; + if (node.hdrin) { // if we removed the header line then shift the counts by 1 + newMessage.parts.index -= 1; + newMessage.parts.count -= 1; + } + } + if (last) { newMessage.complete = true; } + send(newMessage); } - else { - newMessage.parts.index -= node.skip; - newMessage.parts.count -= node.skip; - if (node.hdrin) { // if we removed the header line then shift the counts by 1 - newMessage.parts.index -= 1; - newMessage.parts.count -= 1; + if (has_parts && last && len === 0) { + send({complete:true}); + } + } + node.linecount = 0; + done(); + } + catch(e) { done(e); } + } + else { node.warn(RED._("csv.errors.csv_js")); done(); } + } + else { + if (!msg.hasOwnProperty("reset")) { + node.send(msg); // If no payload and not reset - just pass it on. + } + done(); + } + }); + } + + if(RFC4180Mode) { + node.template = (n.temp || "") + node.sep = (n.sep || ',').replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r") + node.quo = '"' + // default to CRLF (RFC4180 Sec 2.1: "Each record is located on a separate line, delimited by a line break (CRLF)") + node.ret = (n.ret || "\r\n").replace(/\\n/g, "\n").replace(/\\r/g, "\r") + node.multi = n.multi || "one" + node.hdrin = n.hdrin || false + node.hdrout = n.hdrout || "none" + node.goodtmpl = true + node.skip = parseInt(n.skip || 0) + node.store = [] + node.parsestrings = n.strings + node.include_empty_strings = n.include_empty_strings || false + node.include_null_values = n.include_null_values || false + if (node.parsestrings === undefined) { node.parsestrings = true } + if (node.hdrout === false) { node.hdrout = "none" } + if (node.hdrout === true) { node.hdrout = "all" } + const dontSendHeaders = node.hdrout === "none" + const sendHeadersOnce = node.hdrout === "once" + const sendHeadersAlways = node.hdrout === "all" + const sendHeaders = !dontSendHeaders && (sendHeadersOnce || sendHeadersAlways) + const quoteables = [node.sep, node.quo, "\n", "\r"] + const templateQuoteables = [',', '"', "\n", "\r"] + let badTemplateWarnOnce = true + + const columnStringToTemplateArray = function (col, sep) { + // NOTE: enforce strict column template parsing in RFC4180 mode + const parsed = csv.parse(col, { separator: sep, quote: node.quo, outputStyle: 'array', strict: true }) + if (parsed.headers.length > 0) { node.goodtmpl = true } else { node.goodtmpl = false } + return parsed.headers.length ? parsed.headers : null + } + const templateArrayToColumnString = function (template, keepEmptyColumns) { + // NOTE: enforce strict column template parsing in RFC4180 mode + const parsed = csv.parse('', {headers: template, headersOnly:true, separator: ',', quote: node.quo, outputStyle: 'array', strict: true }) + return keepEmptyColumns + ? parsed.headers.map(e => addQuotes(e || '', { separator: ',', quoteables: templateQuoteables})) + : parsed.header // exclues empty columns + // TODO: resolve inconsistency between CSV->JSON and JSON->CSV + // CSV->JSON: empty columns are excluded + // JSON->CSV: empty columns are kept in some cases + } + function addQuotes(cell, options) { + options = options || {} + return csv.quoteCell(cell, { + quote: options.quote || node.quo || '"', + separator: options.separator || node.sep || ',', + quoteables: options.quoteables || quoteables + }) + } + const hasTemplate = (t) => t?.length > 0 && !(t.length === 1 && t[0] === '') + let template + try { + template = columnStringToTemplateArray(node.template, ',') || [''] + } catch (e) { + node.warn(RED._("csv.errors.bad_template")) // is warning really necessary now we have status? + node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") }) + return // dont hook up the node + } + const noTemplate = hasTemplate(template) === false + node.hdrSent = false + + node.on("input", function (msg, send, done) { + node.status({}) // clear status + if (msg.hasOwnProperty("reset")) { + node.hdrSent = false + } + if (msg.hasOwnProperty("payload")) { + let inputData = msg.payload + if (typeof inputData == "object") { // convert object to CSV string + try { + // first determine the payload kind. Array or objects? Array of primitives? Array of arrays? Just an object? + // then, if necessary, convert to an array of objects/arrays + let isObject = !Array.isArray(inputData) && typeof inputData === 'object' + let isArrayOfObjects = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] === 'object' + let isArrayOfArrays = Array.isArray(inputData) && inputData.length > 0 && Array.isArray(inputData[0]) + let isArrayOfPrimitives = Array.isArray(inputData) && inputData.length > 0 && typeof inputData[0] !== 'object' + + if (isObject) { + inputData = [inputData] + isArrayOfObjects = true + isObject = false + } else if (isArrayOfPrimitives) { + inputData = [inputData] + isArrayOfArrays = true + isArrayOfPrimitives = false + } + + const stringBuilder = [] + if (!(noTemplate && (msg.hasOwnProperty("parts") && msg.parts.hasOwnProperty("index") && msg.parts.index > 0))) { + template = columnStringToTemplateArray(node.template) || [''] + } + + // build header line + if (sendHeaders && node.hdrSent === false) { + if (hasTemplate(template) === false) { + if (msg.hasOwnProperty("columns")) { + template = columnStringToTemplateArray(msg.columns || "", ",") || [''] + } + else { + template = Object.keys(inputData[0]) || [''] } } - if (last) { newMessage.complete = true; } - send(newMessage); + stringBuilder.push(templateArrayToColumnString(template, true)) + if (sendHeadersOnce) { node.hdrSent = true } } - if (has_parts && last && len === 0) { - send({complete:true}); + + // build csv lines + for (let s = 0; s < inputData.length; s++) { + let row = inputData[s] + if (isArrayOfArrays) { + /*** row is an array of arrays ***/ + const _hasTemplate = hasTemplate(template) + const len = _hasTemplate ? template.length : row.length + const result = [] + for (let t = 0; t < len; t++) { + let cell = row[t] + if (cell === undefined) { cell = "" } + if(_hasTemplate) { + const header = template[t] + if (header) { + result[t] = addQuotes(RED.util.ensureString(cell)) + } + } else { + result[t] = addQuotes(RED.util.ensureString(cell)) + } + } + stringBuilder.push(result.join(node.sep)) + } else { + /*** row is an object ***/ + if (hasTemplate(template) === false && (msg.hasOwnProperty("columns"))) { + template = columnStringToTemplateArray(msg.columns || "", ",") + } + if (hasTemplate(template) === false) { + /*** row is an object but we still don't have a template ***/ + if (badTemplateWarnOnce === true) { + node.warn(RED._("csv.errors.obj_csv")) + badTemplateWarnOnce = false + } + const rowData = [] + for (let header in inputData[0]) { + if (row.hasOwnProperty(header)) { + const cell = row[header] + if (typeof cell !== "object") { + let cellValue = "" + if (cell !== undefined) { + cellValue += cell + } + rowData.push(addQuotes(cellValue)) + } + } + } + stringBuilder.push(rowData.join(node.sep)) + } else { + /*** row is an object and we have a template ***/ + const rowData = [] + for (let t = 0; t < template.length; t++) { + if (!template[t]) { + rowData.push('') + } + else { + let cellValue = inputData[s][template[t]] + if (cellValue === undefined) { cellValue = "" } + cellValue = RED.util.ensureString(cellValue) + rowData.push(addQuotes(cellValue)) + } + } + stringBuilder.push(rowData.join(node.sep)); // add separator + } + } } + + // join lines, don't forget to add the last new line + msg.payload = stringBuilder.join(node.ret) + node.ret + msg.columns = templateArrayToColumnString(template) + if (msg.payload !== '') { send(msg) } + done() + } + catch (e) { + done(e) } - node.linecount = 0; - done(); } - catch(e) { done(e); } + else if (typeof inputData == "string") { // convert CSV string to object + try { + let firstLine = true; // is this the first line + let last = false + let linecount = 0 + const has_parts = msg.hasOwnProperty("parts") + + // determine if this is a multi part message and if so what part we are processing + if (msg.hasOwnProperty("parts")) { + linecount = msg.parts.index + if (msg.parts.index > node.skip) { firstLine = false } + if (msg.parts.hasOwnProperty("count") && (msg.parts.index + 1 >= msg.parts.count)) { last = true } + } + + // If skip is set, compute the cursor position to start parsing from + let _cursor = 0 + if (node.skip > 0 && linecount < node.skip) { + for (; _cursor < inputData.length; _cursor++) { + if (firstLine && (linecount < node.skip)) { + if (inputData[_cursor] === "\r" || inputData[_cursor] === "\n") { + linecount += 1 + } + continue + } + break + } + if (_cursor >= inputData.length) { + return // skip this line + } + } + + // count the number of line breaks in the string + const noofCR = ((_cursor ? inputData.slice(_cursor) : inputData).match(/[\r\n]/g) || []).length + + // if we have `parts` and we are outputting multiple objects and we have more than one line + // then we need to set firstLine to true so that we process the header line + if (has_parts && node.multi === "mult" && noofCR > 1) { + firstLine = true + } + + // if we are processing the first line and the node has been set to extract the header line + // update the template with the header line + if (firstLine && node.hdrin === true) { + /** @type {import('./lib/csv/index.js').CSVParseOptions} */ + const csvOptionsForHeaderRow = { + cursor: _cursor, + separator: node.sep, + quote: node.quo, + dataHasHeaderRow: true, + headersOnly: true, + outputStyle: 'array', + strict: true // enforce strict parsing of the header row + } + try { + const csvHeader = csv.parse(inputData, csvOptionsForHeaderRow) + template = csvHeader.headers + _cursor = csvHeader.cursor + } catch (e) { + // node.warn(RED._("csv.errors.bad_template")) // add warning? + node.status({ fill: "red", shape: "dot", text: RED._("csv.errors.bad_template") }) + throw e + } + } + + // now we process the data lines + /** @type {import('./lib/csv/index.js').CSVParseOptions} */ + const csvOptions = { + cursor: _cursor, + separator: node.sep, + quote: node.quo, + dataHasHeaderRow: false, + headers: hasTemplate(template) ? template : null, + outputStyle: 'object', + includeNullValues: node.include_null_values, + includeEmptyStrings: node.include_empty_strings, + parseNumeric: node.parsestrings, + strict: false // relax the strictness of the parser for data rows + } + const csvParseResult = csv.parse(inputData, csvOptions) + const data = csvParseResult.data + + // output results + if (node.multi !== "one") { + if (has_parts && noofCR <= 1) { + if (data.length > 0) { + node.store.push(...data) + } + if (msg.parts.index + 1 === msg.parts.count) { + msg.payload = node.store + msg.columns = csvParseResult.header + // msg._mode = 'RFC4180 mode' + delete msg.parts + send(msg) + node.store = [] + } + } + else { + msg.columns = csvParseResult.header + // msg._mode = 'RFC4180 mode' + msg.payload = data + send(msg); // finally send the array + } + } + else { + const len = data.length + for (let row = 0; row < len; row++) { + const newMessage = RED.util.cloneMessage(msg) + newMessage.columns = csvParseResult.header + newMessage.payload = data[row] + if (!has_parts) { + newMessage.parts = { + id: msg._msgid, + index: row, + count: len + } + } + else { + newMessage.parts.index -= node.skip + newMessage.parts.count -= node.skip + if (node.hdrin) { // if we removed the header line then shift the counts by 1 + newMessage.parts.index -= 1 + newMessage.parts.count -= 1 + } + } + if (last) { newMessage.complete = true } + // newMessage._mode = 'RFC4180 mode' + send(newMessage) + } + if (has_parts && last && len === 0) { + // send({complete:true, _mode: 'RFC4180 mode'}) + send({ complete: true }) + } + } + + node.linecount = 0 + done() + } + catch (e) { + done(e) + } + } + else { + // RFC-vs-legacy mode difference: In RFC mode, we throw catchable errors and provide a status message + const err = new Error(RED._("csv.errors.csv_js")) + node.status({ fill: "red", shape: "dot", text: err.message }) + done(err) + } } - else { node.warn(RED._("csv.errors.csv_js")); done(); } - } - else { - if (!msg.hasOwnProperty("reset")) { - node.send(msg); // If no payload and not reset - just pass it on. + else { + if (!msg.hasOwnProperty("reset")) { + node.send(msg); // If no payload and not reset - just pass it on. + } + done() } - done(); - } - }); + }) + } } - RED.nodes.registerType("csv",CSVNode); + + RED.nodes.registerType("csv",CSVNode) } diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.html b/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.html index 3357ae011..cda57386e 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.html +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.html @@ -14,6 +14,7 @@ +
@@ -28,6 +29,10 @@ +
@@ -45,7 +50,8 @@ outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) }, tag: {value:""}, ret: {value:"html"}, - as: {value:"single"} + as: {value:"single"}, + chr: { value: "_" } }, inputs:1, outputs:1, @@ -59,6 +65,13 @@ oneditprepare: function() { $("#node-input-property").typedInput({default:'msg',types:['msg']}); $("#node-input-outproperty").typedInput({default:'msg',types:['msg']}); + $('#node-input-ret').on( 'change', () => { + if ( $('#node-input-ret').val() == "compl" ) { + $('#html-prefix-row').show() + } else { + $('#html-prefix-row').hide() + } + }); } }); diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.js b/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.js index 073b98689..c4485fe9b 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.js +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-HTML.js @@ -25,6 +25,7 @@ module.exports = function(RED) { this.tag = n.tag; this.ret = n.ret || "html"; this.as = n.as || "single"; + this.chr = n.chr || "_"; var node = this; this.on("input", function(msg,send,done) { var value = RED.util.getMessageProperty(msg,node.property); @@ -47,6 +48,11 @@ module.exports = function(RED) { if (node.ret === "attr") { pay2 = Object.assign({},this.attribs); } + if (node.ret === "compl") { + var bse = {} + bse[node.chr] = $(this).html().trim() + pay2 = Object.assign(bse, this.attribs); + } //if (node.ret === "val") { pay2 = $(this).val(); } /* istanbul ignore else */ if (pay2) { @@ -69,6 +75,11 @@ module.exports = function(RED) { var attribs = Object.assign({},this.attribs); pay.push( attribs ); } + if (node.ret === "compl") { + var bse = {} + bse[node.chr] = $(this).html().trim() + pay.push( Object.assign(bse, this.attribs) ) + } //if (node.ret === "val") { pay.push( $(this).val() ); } } index++; diff --git a/packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js b/packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js new file mode 100644 index 000000000..73cf4b292 --- /dev/null +++ b/packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js @@ -0,0 +1,324 @@ + +/** + * @typedef {Object} CSVParseOptions + * @property {number} [cursor=0] - an index into the CSV to start parsing from + * @property {string} [separator=','] - the separator character + * @property {string} [quote='"'] - the quote character + * @property {boolean} [headersOnly=false] - only parse the headers and return them + * @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data + * @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row + * @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output) + * @property {boolean} [parseNumeric=false] - parse numeric values into numbers + * @property {boolean} [includeNullValues=false] - include null values in the output + * @property {boolean} [includeEmptyStrings=true] - include empty strings in the output + * @property {string} [outputStyle='object'] - output an array of arrays or an array of objects + * @property {boolean} [strict=false] - throw an error if the CSV is malformed + */ + +/** + * Parses a CSV string into an array of arrays or an array of objects. + * + * NOTES: + * * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will: + * * accept any separator character, not just `,` + * * accept any quote character, not just `"` + * * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF) + * * Only single character `quote` is supported + * * `quote` is `"` by default + * * Any cell that contains a `quote` or `separator` will be quoted + * * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6 + * * Only single character `separator` is supported + * * Only `array` and `object` output styles are supported + * * `array` output style is an array of arrays [[],[],[]] + * * `object` output style is an array of objects [{},{},{}] + * * Only `headers` or `dataHasHeaderRow` are supported, not both + * @param {string} csvIn - the CSV string to parse + * @param {CSVParseOptions} parseOptions - options + * @throws {Error} + */ +function parse(csvIn, parseOptions) { + /* Normalise options */ + parseOptions = parseOptions || {}; + const separator = parseOptions.separator ?? ','; + const quote = parseOptions.quote ?? '"'; + const headersOnly = parseOptions.headersOnly ?? false; + const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : [] + const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true; + const outputHeader = parseOptions.outputHeader ?? true; + const parseNumeric = parseOptions.parseNumeric ?? false; + const includeNullValues = parseOptions.includeNullValues ?? false; + const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true; + const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}] + const strict = parseOptions.strict ?? false + + /* Local variables */ + const cursorMax = csvIn.length; + const ouputArrays = outputStyle === 'array'; + const headersSupplied = headers.length > 0 + // The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i; + // Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat + // and has the benefit of understanding hexadecimals, binary and octal numbers. + const skipNumberConversion = /^ *(\+|-0\d|0\d)/ + const cellBuilder = [] + let rowBuilder = [] + let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0; + let newCell = true, inQuote = false, closed = false, output = []; + + /* inline helper functions */ + const finaliseCell = () => { + let cell = cellBuilder.join('') + cellBuilder.length = 0 + // push the cell: + // NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null` + // otherwise push empty string + return rowBuilder.push(cell || (newCell ? null : '')) + } + const finaliseRow = () => { + if (cellBuilder.length) { + finaliseCell() + } + if (rowBuilder.length) { + output.push(rowBuilder) + rowBuilder = [] + } + } + + /* Main parsing loop */ + while (cursor < cursorMax) { + const char = csvIn[cursor] + if (inQuote) { + if (char === quote && csvIn[cursor + 1] === quote) { + cellBuilder.push(quote) + cursor += 2; + newCell = false; + closed = false; + } else if (char === quote) { + inQuote = false; + cursor += 1; + newCell = false; + closed = true; + } else { + cellBuilder.push(char) + newCell = false; + closed = false; + cursor++; + } + } else { + if (char === separator) { + finaliseCell() + cursor += 1; + newCell = true; + closed = false; + } else if (char === quote) { + if (newCell) { + inQuote = true; + cursor += 1; + newCell = false; + closed = false; + } + else if (strict) { + throw new UnquotedQuoteError(cursor) + } else { + // not strict, keep 1 quote if the next char is not a cell/record separator + cursor++ + if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) { + cellBuilder.push(char) + if (csvIn[cursor] === quote) { + cursor++ // skip the next quote + } + } + } + } else { + if (char === '\n' || char === '\r') { + finaliseRow() + if (csvIn[cursor + 1] === '\n') { + cursor += 2; + } else { + cursor++ + } + newCell = true; + closed = false; + if (headersOnly) { + break + } + } else { + if (closed) { + if (strict) { + throw new DataAfterCloseError(cursor) + } else { + cursor--; // move back to grab the previously discarded char + closed = false + } + } else { + cellBuilder.push(char) + newCell = false; + cursor++; + } + } + } + } + } + if (strict && inQuote) { + throw new ParseError(`Missing quote, unclosed cell`, cursor) + } + // finalise the last cell/row + finaliseRow() + let firstRowIsHeader = false + // if no headers supplied, generate them + if (output.length >= 1) { + if (headersSupplied) { + // headers already supplied + } else if (dataHasHeaderRow) { + // take the first row as the headers + headers.push(...output[0]) + firstRowIsHeader = true + } else { + // generate headers col1, col2, col3, etc + for (let i = 0; i < output[0].length; i++) { + headers.push("col" + (i + 1)) + } + } + } + + const finalResult = { + /** @type {String[]} headers as an array of string */ + headers: headers, + /** @type {String} headers as a comma-separated string */ + header: null, + /** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */ + data: [], + /** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */ + firstRowIsHeader: undefined, + /** @type {'array'|'object'} flag to indicate the output style */ + outputStyle: outputStyle, + /** @type {Number} The current cursor position */ + cursor: cursor, + } + + const quotedHeaders = [] + for (let i = 0; i < headers.length; i++) { + if (!headers[i]) { + continue + } + quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' })) + } + finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma + + // output is an array of arrays [[],[],[]] + if (ouputArrays || headersOnly) { + if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) { + if (output.length > 0) { + output.unshift(headers) + } else { + output = [headers] + } + firstRowIsHeader = true + } + if (headersOnly) { + delete finalResult.firstRowIsHeader + return finalResult + } + finalResult.firstRowIsHeader = firstRowIsHeader + finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output + return finalResult + } + + // output is an array of objects [{},{},{}] + const outputObjects = [] + let i = firstRowIsHeader ? 1 : 0 + for (; i < output.length; i++) { + const rowObject = {} + let isEmpty = true + for (let j = 0; j < headers.length; j++) { + if (!headers[j]) { + continue + } + let v = output[i][j] === undefined ? null : output[i][j] + if (v === null && !includeNullValues) { + continue + } else if (v === "" && !includeEmptyStrings) { + continue + } else if (parseNumeric === true && v && !skipNumberConversion.test(v)) { + const vTemp = +v + const isNumber = !isNaN(vTemp) + if(isNumber) { + v = vTemp + } + } + rowObject[headers[j]] = v + isEmpty = false + } + // determine if this row is empty + if (!isEmpty) { + outputObjects.push(rowObject) + } + } + finalResult.data = outputObjects + delete finalResult.firstRowIsHeader + return finalResult +} + +/** + * Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2). + * @param {string} cell - the string to quote + * @param {*} options - options + * @param {string} [options.quote='"'] - the quote character + * @param {string} [options.separator=','] - the separator character + * @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes + * @returns + */ +function quoteCell(cell, { quote = '"', separator = ",", quoteables } = { + quote: '"', + separator: ",", + quoteables: [quote, separator, '\r', '\n'] + }) { + quoteables = quoteables || [quote, separator, '\r', '\n']; + + let doubleUp = false; + if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes + doubleUp = true; + } + const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : ''; + return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar; +} + +// #region Custom Error Classes +class ParseError extends Error { + /** + * @param {string} message - the error message + * @param {number} cursor - the cursor index where the error occurred + */ + constructor(message, cursor) { + super(message) + this.name = 'ParseError' + this.cursor = cursor + } +} + +class UnquotedQuoteError extends ParseError { + /** + * @param {number} cursor - the cursor index where the error occurred + */ + constructor(cursor) { + super('Quote found in the middle of an unquoted field', cursor) + this.name = 'UnquotedQuoteError' + } +} + +class DataAfterCloseError extends ParseError { + /** + * @param {number} cursor - the cursor index where the error occurred + */ + constructor(cursor) { + super('Data found after closing quote', cursor) + this.name = 'DataAfterCloseError' + } +} + +// #endregion + +exports.parse = parse +exports.quoteCell = quoteCell +exports.ParseError = ParseError +exports.UnquotedQuoteError = UnquotedQuoteError +exports.DataAfterCloseError = DataAfterCloseError diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html index c32703acb..c71d7ad84 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.html +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.html @@ -15,7 +15,11 @@ --> @@ -57,7 +60,8 @@ arraySplt: {value:1}, arraySpltType: {value:"len"}, stream: {value:false}, - addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })} + addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}, + property: {value:"payload",required:true} }, inputs:1, outputs:1, @@ -69,6 +73,10 @@ return this.name?"node_label_italic":""; }, oneditprepare: function() { + if (this.property === undefined) { + $("#node-input-property").val("payload"); + } + $("#node-input-property").typedInput({default:'msg',types:['msg']}); $("#node-input-splt").typedInput({ default: 'str', typeField: $("#node-input-spltType"), diff --git a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js index 10c696b76..6e9add270 100644 --- a/packages/node_modules/@node-red/nodes/core/sequence/17-split.js +++ b/packages/node_modules/@node-red/nodes/core/sequence/17-split.js @@ -19,13 +19,13 @@ module.exports = function(RED) { function sendArray(node,msg,array,send) { for (var i = 0; i < array.length-1; i++) { - msg.payload = array[i]; + RED.util.setMessageProperty(msg,node.property,array[i]); msg.parts.index = node.c++; if (node.stream !== true) { msg.parts.count = array.length; } send(RED.util.cloneMessage(msg)); } if (node.stream !== true) { - msg.payload = array[i]; + RED.util.setMessageProperty(msg,node.property,array[i]); msg.parts.index = node.c++; msg.parts.count = array.length; send(RED.util.cloneMessage(msg)); @@ -40,10 +40,12 @@ module.exports = function(RED) { node.stream = n.stream; node.spltType = n.spltType || "str"; node.addname = n.addname || ""; + node.property = n.property||"payload"; try { if (node.spltType === "str") { this.splt = (n.splt || "\\n").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0"); - } else if (node.spltType === "bin") { + } + else if (node.spltType === "bin") { var spltArray = JSON.parse(n.splt); if (Array.isArray(spltArray)) { this.splt = Buffer.from(spltArray); @@ -51,7 +53,8 @@ module.exports = function(RED) { throw new Error("not an array"); } this.spltBuffer = spltArray; - } else if (node.spltType === "len") { + } + else if (node.spltType === "len") { this.splt = parseInt(n.splt); if (isNaN(this.splt) || this.splt < 1) { throw new Error("invalid split length: "+n.splt); @@ -69,18 +72,22 @@ module.exports = function(RED) { node.buffer = Buffer.from([]); node.pendingDones = []; this.on("input", function(msg, send, done) { - if (msg.hasOwnProperty("payload")) { + var value = RED.util.getMessageProperty(msg,node.property); + if (value !== undefined) { if (msg.hasOwnProperty("parts")) { msg.parts = { parts:msg.parts }; } // push existing parts to a stack else { msg.parts = {}; } msg.parts.id = RED.util.generateId(); // generate a random id + if (node.property !== "payload") { + msg.parts.property = node.property; + } delete msg._msgid; - if (typeof msg.payload === "string") { // Split String into array - msg.payload = (node.remainder || "") + msg.payload; + if (typeof value === "string") { // Split String into array + value = (node.remainder || "") + value; msg.parts.type = "string"; if (node.spltType === "len") { msg.parts.ch = ""; msg.parts.len = node.splt; - var count = msg.payload.length/node.splt; + var count = value.length/node.splt; if (Math.floor(count) !== count) { count = Math.ceil(count); } @@ -89,9 +96,9 @@ module.exports = function(RED) { node.c = 0; } var pos = 0; - var data = msg.payload; + var data = value; for (var i=0; i d()); @@ -119,47 +126,48 @@ module.exports = function(RED) { if (!node.spltBufferString) { node.spltBufferString = node.splt.toString(); } - a = msg.payload.split(node.spltBufferString); + a = value.split(node.spltBufferString); msg.parts.ch = node.spltBuffer; // pass the split char to other end for rejoin } else if (node.spltType === "str") { - a = msg.payload.split(node.splt); + a = value.split(node.splt); msg.parts.ch = node.splt; // pass the split char to other end for rejoin } sendArray(node,msg,a,send); done(); } } - else if (Array.isArray(msg.payload)) { // then split array into messages + else if (Array.isArray(value)) { // then split array into messages msg.parts.type = "array"; - var count = msg.payload.length/node.arraySplt; + var count = value.length/node.arraySplt; if (Math.floor(count) !== count) { count = Math.ceil(count); } msg.parts.count = count; var pos = 0; - var data = msg.payload; + var data = value; msg.parts.len = node.arraySplt; for (var i=0; i d()); @@ -230,7 +238,7 @@ module.exports = function(RED) { var i = 0, p = 0; pos = buff.indexOf(node.splt); while (pos > -1) { - msg.payload = buff.slice(p,pos); + RED.util.setMessageProperty(msg,node.property,buff.slice(p,pos)); msg.parts.index = node.c++; send(RED.util.cloneMessage(msg)); i++; @@ -242,7 +250,7 @@ module.exports = function(RED) { node.pendingDones = []; } if ((node.stream !== true) && (p < buff.length)) { - msg.payload = buff.slice(p,buff.length); + RED.util.setMessageProperty(msg,node.property,buff.slice(p,buff.length)); msg.parts.index = node.c++; msg.parts.count = node.c++; send(RED.util.cloneMessage(msg)); @@ -298,7 +306,6 @@ module.exports = function(RED) { return exp } - function reduceMessageGroup(node,msgInfos,exp,fixup,count,accumulator,done) { var msgInfo = msgInfos.shift(); exp.assign("I", msgInfo.msg.parts.index); @@ -330,6 +337,7 @@ module.exports = function(RED) { } }); } + function reduceAndSendGroup(node, group, done) { var is_right = node.reduce_right; var flag = is_right ? -1 : 1; @@ -515,13 +523,13 @@ module.exports = function(RED) { if (typeof group.joinChar !== 'string') { groupJoinChar = group.joinChar.toString(); } - RED.util.setMessageProperty(group.msg,node.property,group.payload.join(groupJoinChar)); + RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload.join(groupJoinChar)); } else { if (node.propertyType === 'full') { group.msg = RED.util.cloneMessage(group.msg); } - RED.util.setMessageProperty(group.msg,node.property,group.payload); + RED.util.setMessageProperty(group.msg,group?.prop||"payload",group.payload); } if (group.msg.hasOwnProperty('parts') && group.msg.parts.hasOwnProperty('parts')) { group.msg.parts = group.msg.parts.parts; @@ -589,7 +597,7 @@ module.exports = function(RED) { } if (node.mode === 'auto' && (!msg.hasOwnProperty("parts")||!msg.parts.hasOwnProperty("id"))) { - // if a blank reset messag erest it all. + // if a blank reset message reset it all. if (msg.hasOwnProperty("reset")) { if (inflight && inflight.hasOwnProperty("partId") && inflight[partId].timeout) { clearTimeout(inflight[partId].timeout); @@ -603,6 +611,15 @@ module.exports = function(RED) { return; } + if (node.mode === 'custom' && msg.hasOwnProperty('parts')) { + if (msg.parts.hasOwnProperty('parts')) { + msg.parts = { parts: msg.parts.parts }; + } + else { + delete msg.parts; + } + } + var payloadType; var propertyKey; var targetCount; @@ -618,6 +635,7 @@ module.exports = function(RED) { propertyKey = msg.parts.key; arrayLen = msg.parts.len; propertyIndex = msg.parts.index; + property = RED.util.getMessageProperty(msg,msg.parts.property||"payload"); } else if (node.mode === 'reduce') { return processReduceMessageQueue({msg, send, done}); @@ -719,6 +737,8 @@ module.exports = function(RED) { completeSend(partId) }, node.timer) } + if (node.mode === "auto") { inflight[partId].prop = msg.parts.property; } + else { inflight[partId].prop = node.property; } } inflight[partId].dones.push(done); diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index a7b583878..99e9cfdc5 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -849,7 +849,13 @@ "newline": "Newline", "usestrings": "parse numerical values", "include_empty_strings": "include empty strings", - "include_null_values": "include null values" + "include_null_values": "include null values", + "spec": "Parser" + }, + "spec": { + "rfc": "RFC4180", + "legacy": "Legacy", + "legacy_warning": "Legacy mode will be removed in a future release." }, "placeholder": { "columns": "comma-separated column names" @@ -878,6 +884,7 @@ "once": "send headers once, until msg.reset" }, "errors": { + "bad_template": "Malformed columns template.", "csv_js": "This node only handles CSV strings or js objects.", "obj_csv": "No columns template specified for object -> CSV.", "bad_csv": "Malformed CSV data - output probably corrupt." @@ -887,12 +894,14 @@ "label": { "select": "Selector", "output": "Output", - "in": "in" + "in": "in", + "prefix": "Property name for HTML content" }, "output": { "html": "the html content of the elements", "text": "only the text content of the elements", - "attr": "an object of any attributes of the elements" + "attr": "an object of any attributes of the elements", + "compl": "an object of any attributes of the elements and html contents" }, "format": { "single": "as a single message containing an array", @@ -1001,7 +1010,7 @@ "tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process." }, "split": { - "split": "split", + "split": "Split", "intro": "Split msg.payload based on type:", "object": "Object", "objectSend": "Send a message for each key/value pair", diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-CSV.html b/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-CSV.html index baa3b036b..56b6d7cca 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-CSV.html +++ b/packages/node_modules/@node-red/nodes/locales/en-US/parsers/70-CSV.html @@ -36,7 +36,9 @@

Details

The column template can contain an ordered list of column names. When converting CSV to an object, the column names - will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV.

+ will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV. +

When the RFC parser is selected, the column template must be compliant with RFC4180.

+

When converting to CSV, the columns template is used to identify which properties to extract from the object and in what order.

If the columns template is blank then you can use a simple comma separated list of properties supplied in msg.columns to determine what to extract and in what order. If neither are present then all the object properties are output in the order @@ -49,4 +51,5 @@

If outputting multiple messages they will have their parts property set and form a complete message sequence.

If the node is set to only send column headers once, then setting msg.reset to any value will cause the node to resend the headers.

Note: the column template must be comma separated - even if a different separator is chosen for the data.

+

Note: in RFC mode, catchable errors will be thrown for malformed CSV headers and invalid input payload data

diff --git a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html index 938a77818..31b43c764 100644 --- a/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html +++ b/packages/node_modules/@node-red/nodes/locales/zh-CN/function/80-template.html @@ -23,7 +23,7 @@
template string
msg.payload填充的模板。如果未在编辑面板中配置,则可以将设为msg的属性。
-

Outputs

+

输出

msg object
由来自传入msg的属性来填充已配置的模板后输出的带有属性的msg。
@@ -32,7 +32,7 @@

默认情况下使用mustache格式。如有需要也可以切换其他格式。

例如:

Hello {{payload.name}}. Today is {{date}}
-

receives a message containing: +

接收一条消息,其中包含:

{
   date: "Monday",
   payload: {
diff --git a/packages/node_modules/@node-red/registry/lib/library.js b/packages/node_modules/@node-red/registry/lib/library.js
index 0b242b270..07cb54318 100644
--- a/packages/node_modules/@node-red/registry/lib/library.js
+++ b/packages/node_modules/@node-red/registry/lib/library.js
@@ -36,7 +36,7 @@ async function getFlowsFromPath(path) {
                     promises.push(getFlowsFromPath(fullPath));
                 } else if (/\.json$/.test(file)){
                     validFiles.push(file);
-                    promises.push(Promise.resolve(file.split(".")[0]))
+                    promises.push(Promise.resolve(file.replace(/\.json$/, '')))
                 }
             })
         }
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
index b541a9d95..dd3d335a2 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -485,7 +485,7 @@ class Flow {
         }
         if (!key.startsWith("$parent.")) {
             if (this._env.hasOwnProperty(key)) {
-                return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
+                return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
             }
         } else {
                 key = key.substring(8);
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Group.js b/packages/node_modules/@node-red/runtime/lib/flows/Group.js
index 521b6ceda..90d93cf45 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Group.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Group.js
@@ -41,7 +41,7 @@ class Group {
         }
         if (!key.startsWith("$parent.")) {
             if (this._env.hasOwnProperty(key)) {
-                return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
+                return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
             }
         } else {
             key = key.substring(8);
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js
index 62948d203..c3a47e1f7 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js
@@ -376,7 +376,7 @@ class Subflow extends Flow {
         }
         if (!key.startsWith("$parent.")) {
             if (this._env.hasOwnProperty(key)) {
-                return (Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
+                return (this._env[key] && Object.hasOwn(this._env[key], 'value') && this._env[key].__clone__) ? clone(this._env[key].value) : this._env[key]
             }
         } else {
             key = key.substring(8);
diff --git a/packages/node_modules/@node-red/runtime/lib/storage/index.js b/packages/node_modules/@node-red/runtime/lib/storage/index.js
index f5e07e254..989989e1d 100644
--- a/packages/node_modules/@node-red/runtime/lib/storage/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/storage/index.js
@@ -77,7 +77,7 @@ var storageModuleInterface = {
                         flows: flows,
                         credentials: creds
                     };
-                    result.rev = crypto.createHash('md5').update(JSON.stringify(result.flows)).digest("hex");
+                    result.rev = crypto.createHash('sha256').update(JSON.stringify(result.flows)).digest("hex");
                     return result;
                 })
             });
@@ -95,7 +95,7 @@ var storageModuleInterface = {
 
             return credentialSavePromise.then(function() {
                 return storageModule.saveFlows(flows, user).then(function() {
-                    return crypto.createHash('md5').update(JSON.stringify(config.flows)).digest("hex");
+                    return crypto.createHash('sha256').update(JSON.stringify(config.flows)).digest("hex");
                 })
             });
         },
diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js
index ea5ffbbe3..4896789b6 100644
--- a/packages/node_modules/@node-red/util/lib/util.js
+++ b/packages/node_modules/@node-red/util/lib/util.js
@@ -636,7 +636,15 @@ function evaluateNodeProperty(value, type, node, msg, callback) {
     } else if (type === 're') {
         result = new RegExp(value);
     } else if (type === 'date') {
-        result = Date.now();
+        if (!value) {
+            result = Date.now();
+        } else if (value === 'object') {
+            result = new Date()
+        } else if (value === 'iso') {
+            result = (new Date()).toISOString()
+        } else {
+            result = moment().format(value)
+        }
     } else if (type === 'bin') {
         var data = JSON.parse(value);
         if (Array.isArray(data) || (typeof(data) === "string")) {
@@ -769,12 +777,15 @@ function evaluateJSONataExpression(expr,msg,callback) {
             });
         }
     } else {
-        log.warn('Deprecated API warning: Calls to RED.util.evaluateJSONataExpression must include a callback. '+
-                 'This will not be optional in Node-RED 4.0. Please identify the node from the following stack '+
-                 'and check for an update on npm. If none is available, please notify the node author.')
-        log.warn(new Error().stack)
+        const error = new Error('Calls to RED.util.evaluateJSONataExpression must include a callback.')
+        throw error
     }
-    return expr.evaluate(context, bindings, callback);
+
+    expr.evaluate(context, bindings).then(result => {
+        callback(null, result)
+    }).catch(err => {
+        callback(err)
+    })
 }
 
 /**
diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json
index 7b8e87129..be92977de 100644
--- a/packages/node_modules/@node-red/util/package.json
+++ b/packages/node_modules/@node-red/util/package.json
@@ -18,7 +18,7 @@
         "fs-extra": "11.1.1",
         "i18next": "21.10.0",
         "json-stringify-safe": "5.0.1",
-        "jsonata": "1.8.6",
+        "jsonata": "2.0.4",
         "lodash.clonedeep": "^4.5.0",
         "moment": "2.29.4",
         "moment-timezone": "0.5.43"
diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js
index 85b5aec48..35ec090c9 100755
--- a/packages/node_modules/node-red/red.js
+++ b/packages/node_modules/node-red/red.js
@@ -415,9 +415,15 @@ httpsPromise.then(function(startupHttps) {
     if (settings.httpAdminRoot !== false) {
         app.use(settings.httpAdminRoot,RED.httpAdmin);
     }
+
     if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
-        app.use(settings.httpNodeRoot,basicAuthMiddleware(settings.httpNodeAuth.user,settings.httpNodeAuth.pass));
+        if (typeof settings.httpNodeAuth === "function" || Array.isArray(settings.httpNodeAuth)) {
+            app.use(settings.httpNodeRoot, settings.httpNodeAuth);
+        } else {
+            app.use(settings.httpNodeRoot, basicAuthMiddleware(settings.httpNodeAuth.user, settings.httpNodeAuth.pass));
+        }
     }
+
     if (settings.httpNodeRoot !== false) {
         app.use(settings.httpNodeRoot,RED.httpNode);
     }
diff --git a/test/nodes/core/parsers/70-CSV_spec.js b/test/nodes/core/parsers/70-CSV_spec.js
index 681711b3b..f362ecdbf 100644
--- a/test/nodes/core/parsers/70-CSV_spec.js
+++ b/test/nodes/core/parsers/70-CSV_spec.js
@@ -15,12 +15,15 @@
  * limitations under the License.
  **/
 
-// var should = require("should");
-var csvNode = require("nr-test-utils").require("@node-red/nodes/core/parsers/70-CSV.js");
-var helper = require("node-red-node-test-helper");
+// const should = require("should");
+const csvNode = require("nr-test-utils").require("@node-red/nodes/core/parsers/70-CSV.js");
+const statusNode = require("nr-test-utils").require("@node-red/nodes/core/common/25-status.js");
+const functionNode = require("nr-test-utils").require("@node-red/nodes/core/function/10-function.js");
+const delayNode = require("nr-test-utils").require("@node-red/nodes/core/function/89-delay.js");
+const helper = require("node-red-node-test-helper");
 // const { neq } = require("semver");
 
-describe('CSV node', function() {
+describe('CSV node (Legacy Mode)', function() {
 
     before(function(done) {
         helper.startServer(done);
@@ -38,16 +41,20 @@ describe('CSV node', function() {
         var flow = [{id:"csvNode1", type:"csv", name: "csvNode" }];
         helper.load(csvNode, flow, function() {
             var n1 = helper.getNode("csvNode1");
-            n1.should.have.property('name', 'csvNode');
-            n1.should.have.property('template','');
-            n1.should.have.property('sep', ',');
-            n1.should.have.property('quo', '"');
-            n1.should.have.property('ret', '\n');
-            n1.should.have.property('winflag', false);
-            n1.should.have.property('lineend', '\n');
-            n1.should.have.property('multi', 'one');
-            n1.should.have.property('hdrin', false);
-            done();
+            try {
+                n1.should.have.property('name', 'csvNode');
+                n1.should.have.property('template','');
+                n1.should.have.property('sep', ',');
+                n1.should.have.property('quo', '"');
+                n1.should.have.property('ret', '\n');
+                // n1.should.have.property('winflag', false);
+                // n1.should.have.property('lineend', '\n');
+                n1.should.have.property('multi', 'one');
+                n1.should.have.property('hdrin', false);
+                done();
+            } catch (error) {
+                done(error);
+            }
         });
     });
 
@@ -77,10 +84,13 @@ 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: 2, c: 3, d: 4 });
-                    msg.should.have.property('columns', "a,b,c,d");
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
+                        msg.should.have.property('columns', "a,b,c,d");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -94,10 +104,13 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
-                    msg.should.have.property('columns', "col1,col2,col3,col4");
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
+                        msg.should.have.property('columns', "col1,col2,col3,col4");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1|2|3|4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -111,10 +124,13 @@ 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: 2, D: 4 });
-                    msg.should.have.property('columns', "A,B,D");
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { A: 1, B: 2, D: 4 });
+                        msg.should.have.property('columns', "A,B,D");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1\t2\t3\t4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -128,10 +144,12 @@ 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: 2, D: 4 });
-                    msg.should.have.property('columns', "A,B,D");
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { A: 1, B: 2, D: 4 });
+                        msg.should.have.property('columns', "A,B,D");
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
                 });
                 var testString = "1 2 3 4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -145,9 +163,11 @@ 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: 2, c: 3, d: 4 });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -161,10 +181,13 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
-                    msg.should.have.property('columns', "col1,col2,col3,col4");
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
+                        msg.should.have.property('columns', "col1,col2,col3,col4");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -178,10 +201,13 @@ 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, d: 4 });
-                    msg.should.have.property('columns', 'a,d');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 1, d: 4 });
+                        msg.should.have.property('columns', 'a,d');
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -195,10 +221,12 @@ 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"');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        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();
+                    } catch (e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -212,10 +240,12 @@ 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"');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        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();
+                    } catch (e) { done(e); }
                 });
                 var testString = 'a,b b,"c,c"," d, d "'+"\n"+"1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -229,10 +259,12 @@ 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"');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        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();
+                    } catch (e) { done(e); }
                 });
                 var testString = 'a;b b;"c;c";" d, d "'+"\n"+"1;2;3;4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -246,10 +278,12 @@ 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"');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        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();
+                    } catch (e) { done(e); }
                 });
                 var testString = 'a/b b/"c/c"/" d, d "'+"\n"+"1/2/3/4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -263,10 +297,12 @@ 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"');
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        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();
+                    } catch (e) { done(e); }
                 });
                 var testString = 'a\\b b\\"c\\c"\\" d, d "'+"\n"+"1\\2\\3\\4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -280,9 +316,12 @@ 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: 123, b: "0123", c: '+123', d: 'e123', e: 'E123', f: -123 });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 123, b: "0123", c: '+123', d: 'e123', e: 'E123', f: -123 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '123,0123,+123,e123,E123,-123'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -296,9 +335,12 @@ 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.23", b: "0123", c: "+123", d: "e123", e: "0", f: "-123", g: "1e3" });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: "1.23", b: "0123", c: "+123", d: "e123", e: "0", f: "-123", g: "1e3" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '1.23,0123,+123,e123,0,-123,1e3'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -312,9 +354,12 @@ 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.23, b: -123, c: 1000, d: 0 });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 1.23, b: -123, c: 1000, d: 0 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = ' 1.23 ,  -123,1e3 ,    0  '+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -328,9 +373,12 @@ 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: 12000, b: 0.012, c: -12000, d: -0.012 });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 12000, b: 0.012, c: -12000, d: -0.012 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '12E3,12e-3,-12e3,-12E-3'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -345,29 +393,36 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    //console.log(msg);
-                    msg.should.have.property('payload', { a:1, b:-2, c:'+3', d:'04', f:'-05', g:'ab"cd', h:'with,a,comma' });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a:1, b:-2, c:'+3', d:'04', f:'-05', g:'ab"cd', h:'with,a,comma' });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '"1","-2","+3","04","","-05","ab""cd","with,a,comma"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
             });
         });
 
-        it('should allow blank strings in the input if selected', function(done) {
-            var flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", include_empty_strings:true, wires:[["n2"]] },
+        it('should allow blank strings in the input if selected', async function() {
+            const flow = [ { id:"n1", type:"csv", temp:"a,b,c,d,e,f,g", include_empty_strings:true, 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(msg);
-                    msg.should.have.property('payload', { a: 1, b: '', c: '', d: '', e: '-05', f: 'ab"cd', g: 'with,a,comma' });
-                    //check_parts(msg, 0, 1);
-                    done();
+            await helper.load(csvNode, flow) 
+            var n1 = helper.getNode("n1");
+            var n2 = helper.getNode("n2");
+            await new Promise((resolve, reject) => {
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: '', c: '', d: '', e: '-05', f: 'ab"cd', g: 'with,a,comma' });
+                        //check_parts(msg, 0, 1);
+                        resolve()
+                    } catch (err) {
+                        reject(err);
+                    }
                 });
-                var testString = '"1","","","","-05","ab""cd","with,a,comma"'+String.fromCharCode(10);
+                const testString = '"1","","","","-05","ab""cd","with,a,comma"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
             });
         });
@@ -379,10 +434,13 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    //console.log(msg);
-                    msg.should.have.property('payload', { a: 1, b: null, c: '+3', d: null, e: '-05', f: 'ab"cd', g: 'with,a,comma' });
-                    //check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a: 1, b: null, c: '+3', d: null, e: '-05', f: 'ab"cd', g: 'with,a,comma' });
+                        // check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '"1",,"+3",,"-05","ab""cd","with,a,comma"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -396,10 +454,13 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    //console.log(msg);
-                    msg.should.have.property('payload', { a: "with a\nnew line", b: "and a\rcarriage return", c: "and why\r\nnot both"});
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a: "with a\nnew line", b: "and a\rcarriage return", c: "and why\r\nnot both"});
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = '"with a'+String.fromCharCode(10)+'new line","and a'+String.fromCharCode(13)+'carriage return","and why\r\nnot both"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -414,16 +475,19 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    if (c == 0) {
-                        c = 1;
-                        msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\n" });
-                        check_parts(msg, 0, 1);
-                    }
-                    else {
-                        msg.should.have.property('payload', { a: "this is", b: "a normal", c: "line" });
-                        check_parts(msg, 0, 1);
-                        done();
+                    try {
+                        if (c == 0) {
+                            c = 1;
+                            msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\n" });
+                            check_parts(msg, 0, 1);
+                        }
+                        else {
+                            msg.should.have.property('payload', { a: "this is", b: "a normal", c: "line" });
+                            check_parts(msg, 0, 1);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testString = '"with,a"n,odd","num"ber","of"qu"ot"es"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -439,17 +503,20 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    //console.log(msg)
-                    if (c == 0) {
-                        c = 1;
-                        msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\nthis is,a normal,line" });
-                        check_parts(msg, 0, 1);
-                    }
-                    else {
-                        msg.should.have.property('payload', { a: "this is", b: "another", c: "line" });
-                        check_parts(msg, 0, 1);
-                        done();
+                    try {
+                        //console.log(msg)
+                        if (c == 0) {
+                            c = 1;
+                            msg.should.have.property('payload', { a: "with,an", b: "odd,number", c: "ofquotes\nthis is,a normal,line\n" });
+                            check_parts(msg, 0, 1);
+                        }
+                        else {
+                            msg.should.have.property('payload', { a: "this is", b: "another", c: "line" });
+                            check_parts(msg, 0, 1);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testString = '"with,a"n,odd","num"ber","of"qu"ot"es"'+String.fromCharCode(10)+'"this is","a normal","line"'+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -465,17 +532,20 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    //console.log(msg);
-                    if (c === 0) {
-                        msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
-                        check_parts(msg, 0, 2);
-                        c += 1;
-                    }
-                    else {
-                        msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
-                        check_parts(msg, 1, 2);
-                        done();
+                    try {
+                        //console.log(msg);
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testString = "w,x,y,z\n1,2,3,4\n\n5,6,7,8";
                 n1.emit("input", {payload:testString});
@@ -489,10 +559,13 @@ 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: 2, c: 3, d: 4 },{ a: 5, b: -6, c: '07', d: '+8' },{ a: 9, b: 0, c: 'a', d: 'b' },{ a: 'c', b: 'd', c: 'e', d: 'f' } ]);
-                    msg.should.have.property('columns','a,b,c,d');
-                    msg.should.not.have.property('parts');
-                    done();
+                    try {
+                        msg.should.have.property('payload', [ { a: 1, b: 2, c: 3, d: 4 },{ a: 5, b: -6, c: '07', d: '+8' },{ a: 9, b: 0, c: 'a', d: 'b' },{ a: 'c', b: 'd', c: 'e', d: 'f' } ]);
+                        msg.should.have.property('columns','a,b,c,d');
+                        msg.should.not.have.property('parts');
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4\n5,-6,07,+8\n9,0,a,b\nc,d,e,f";
                 n1.emit("input", {payload:testString});
@@ -506,10 +579,13 @@ 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":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]);
-                    msg.should.have.property('columns','a,b,c');
-                    msg.should.not.have.property('parts');
-                    done();
+                    try {
+                        msg.should.have.property('payload', [{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]);
+                        msg.should.have.property('columns','a,b,c');
+                        msg.should.not.have.property('parts');
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
 
                 n1.emit("input", {"payload":"a,b,c","parts":{"index":0,"ch":"\n","type":"string","id":"1"}});
@@ -526,10 +602,13 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    msg.should.have.property('payload', [{"Col1":"V1","Col2":"V2"},{"Col1":"V3","Col2":"V4"},{"Col1":"V5","Col2":"V6"}]);
-                    msg.should.have.property('columns','Col1,Col2');
-                    msg.should.have.property('parts');
-                    done();
+                    try {
+                        msg.should.have.property('payload', [{"Col1":"V1","Col2":"V2"},{"Col1":"V3","Col2":"V4"},{"Col1":"V5","Col2":"V6"}]);
+                        msg.should.have.property('columns','Col1,Col2');
+                        msg.should.have.property('parts');
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 //var testString = "1,2,3,4\n5,-6,07,+8\n9,0,a,b\nc,d,e,f";
                 // n1.emit("input", {payload:testString});
@@ -545,9 +624,12 @@ 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: "a", b: "127.0.0.1", c: 56.7, d: -32.8, e: "+76.22C" });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: "a", b: "127.0.0.1", c: 56.7, d: -32.8, e: "+76.22C" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "a,127.0.0.1,56.7,-32.8,+76.22C";
                 n1.emit("input", {payload:testString});
@@ -561,9 +643,12 @@ 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: 2, c: 3, d: 4 });
-                    check_parts(msg, 3, 4);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
+                        check_parts(msg, 3, 4);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString, parts: {id:"X", index:3, count:4} });
@@ -578,16 +663,19 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    if (c === 0) {
-                        msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
-                        check_parts(msg, 0, 2);
-                        c += 1;
-                    }
-                    else {
-                        msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
-                        check_parts(msg, 1, 2);
-                        done();
+                    try {
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testString1 = "w,x,y,z\n";
                 var testString2 = "1,2,3,4\n";
@@ -605,9 +693,12 @@ 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: 9, b: 0, c: "A", d: "B" });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { a: 9, b: 0, c: "A", d: "B" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10)+"5,6,7,8"+String.fromCharCode(10)+"9,0,A,B"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -621,9 +712,12 @@ describe('CSV node', function() {
                 var n1 = helper.getNode("n1");
                 var n2 = helper.getNode("n2");
                 n2.on("input", function(msg) {
-                    msg.should.have.property('payload', { "9": "C", "0": "D", "A": "E", "B": "F" });
-                    check_parts(msg, 0, 1);
-                    done();
+                    try {
+                        msg.should.have.property('payload', { "9": "C", "0": "D", "A": "E", "B": "F" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10)+"5,6,7,8"+String.fromCharCode(10)+"9,0,A,B"+String.fromCharCode(10)+"C,D,E,F"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -638,16 +732,19 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    if (c===0) {
-                        msg.should.have.property('payload', { a: 9, b: 0, c: "A", d: "B" });
-                        check_parts(msg, 0, 2);
-                        c = c+1;
-                    }
-                    else {
-                        msg.should.have.property('payload', { a: "C", b: "D", c: "E", d: "F" });
-                        check_parts(msg, 1, 2);
-                        done();
+                    try {
+                        if (c===0) {
+                            msg.should.have.property('payload', { a: 9, b: 0, c: "A", d: "B" });
+                            check_parts(msg, 0, 2);
+                            c = c+1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { a: "C", b: "D", c: "E", d: "F" });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testString = "1,2,3,4"+String.fromCharCode(10)+"5,6,7,8"+String.fromCharCode(10)+"9,0,A,B"+String.fromCharCode(10)+"C,D,E,F"+String.fromCharCode(10);
                 n1.emit("input", {payload:testString});
@@ -662,18 +759,21 @@ describe('CSV node', function() {
                 var n2 = helper.getNode("n2");
                 var c = 0;
                 n2.on("input", function(msg) {
-                    if (c === 0) {
-                        msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
-                        msg.should.have.property('columns', 'w,x,y,z');
-                        check_parts(msg, 0, 2);
-                        c += 1;
-                    }
-                    else {
-                        msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
-                        msg.should.have.property('columns', 'w,x,y,z');
-                        check_parts(msg, 1, 2);
-                        done();
+                    try {
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            msg.should.have.property('columns', 'w,x,y,z');
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            msg.should.have.property('columns', 'w,x,y,z');
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
                     }
+                    catch(e) { done(e); }
                 });
                 var testStringA = "foo\n";
                 var testStringB = "bar\n";
@@ -1089,3 +1189,1257 @@ describe('CSV node', function() {
         });
     });
 });
+
+describe('CSV node (RFC Mode)', function () {
+
+    before(function (done) {
+        helper.startServer(done);
+    });
+
+    after(function (done) {
+        helper.stopServer(done);
+    });
+
+    afterEach(function () {
+        helper.unload();
+    });
+
+    it('should be loaded with defaults', function (done) {
+        // RFC-Legacy difference
+        // In RFC mode, the default line separator is \r\n (RFC4180 Section 2.1)
+        // In Legacy mode, the line separator is set to \n
+        const flow = [{ id: "n1", type: "csv", spec: "rfc", name: "csvNode" }];
+        helper.load(csvNode, flow, function () {
+            const n1 = helper.getNode("n1");
+            try {
+                n1.should.have.property('name', 'csvNode');
+                n1.should.have.property('template', '');
+                n1.should.have.property('sep', ',');
+                n1.should.have.property('quo', '"');
+                n1.should.have.property('ret', '\r\n'); // RFC-Legacy difference
+                n1.should.have.property('multi', 'one');
+                n1.should.have.property('hdrin', false);
+                done();
+            } catch (error) {
+                done(error);
+            }
+        });
+    });
+
+    describe('csv to json', function () {
+        let parts_id = undefined;
+
+        afterEach(function () {
+            parts_id = undefined;
+        });
+
+        function check_parts(msg, index, count) {
+            msg.should.have.property('parts');
+            if (parts_id === undefined) {
+                parts_id = msg.parts.id;
+            }
+            else {
+                msg.parts.should.have.property('id', parts_id);
+            }
+            msg.parts.should.have.property('index', index);
+            msg.parts.should.have.property('count', count);
+        }
+
+        it('should convert a simple csv string to a javascript object', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
+                        msg.should.have.property('columns', "a,b,c,d");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should convert a simple string to a javascript object with | separator (no template)', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", sep: "|", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
+                        msg.should.have.property('columns', "col1,col2,col3,col4");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1|2|3|4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should convert a simple string to a javascript object with tab separator (with template)', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", sep: "\t", temp: "A,B,,D", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { A: 1, B: 2, D: 4 });
+                        msg.should.have.property('columns', "A,B,D");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1\t2\t3\t4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should convert a simple string to a javascript object with space separator (with spaced template)', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            // In legacy mode, the template "A, B, , D" is trimmed to "A,B,D"
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", sep: " ", temp: "A, B, , D", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //                       'payload', { A: 1, B: 2, D: 4 } // Legacy
+                        msg.should.have.property('payload', { A: 1, ' B': 2, ' ': 3, ' D': 4 }); // RFC
+                        //                       'columns', "A,B,D" // Legacy
+                        msg.should.have.property('columns', "A, B, , D"); // RFC
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = "1 2 3 4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should not remove quotes and whitespace from template - should set status and send warning', async function () {
+            // RFC-vs-Legacy difference
+            // In RFC mode, the column template is parsed in strict mode
+            // meaning this template is invalid because:
+            //   * it encounters a quote in an unquoted field (the "b" in the 2nd column)
+            //     * The 2nd column starts with a space but then a single quote is encountered. RFC4180 Section 2.6 
+            //       says Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes
+            //   * it contains a data between the closing quote and separator (4th column starts ok but then has a space after the closing quote)
+            //     * Since we adhere to RFC4180 Section 2.4, "Spaces are considered part of a field and should not be ignored"
+            //       we must treat the space as part of the data meaning field is not correctly close with a quote
+            const flow = [
+                { id: "n1", type: "csv", spec: "rfc", temp: '', ret: '\n', wires: [["n2"]] },
+                { id: "n2", type: "helper" }
+            ];
+            await helper.load(csvNode, flow)
+
+            const n1 = helper.getNode("n1")
+            const n2 = helper.getNode("n2")
+
+            //modify the flow and deploy change that sets the csv node to a bad template - thus updating the nodes status and logging a warning
+            const newConfig = flow.map(n => ({ ...n }));
+            newConfig[0].temp = '"a",  "b" , " c "," d  " ';
+            await helper.setFlows(newConfig, "nodes") // deploy "nodes" only
+
+            // small sleep for msg flow and logging to complete
+            await new Promise(resolve => setTimeout(resolve, 50));
+
+            // check that status message was sent
+            n1.status.called.should.be.true();
+            n1.status.lastCall.args[0].should.deepEqual({ fill: 'red', shape: 'dot', text: 'csv.errors.bad_template' });
+
+            // warn message should be logged
+            n1.warn.called.should.be.true();
+            n1.warn.lastCall.args[0].should.equal('csv.errors.bad_template');
+            const logs = helper.log().args.filter(function (evt) {
+                return evt[0].type == "csv";
+            });
+            logs.should.have.length(1);
+            logs[0][0].should.have.a.property('msg');
+            logs[0][0].msg.should.equal('csv.errors.bad_template');
+        });
+
+        it('should create column names if no template provided', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: '', wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { col1: 1, col2: 2, col3: 3, col4: 4 });
+                        msg.should.have.property('columns', "col1,col2,col3,col4");
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow dropping of fields from the template', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,,,d", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, d: 4 });
+                        msg.should.have.property('columns', 'a,d');
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow commas and spaces in the template', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            // In legacy mode, the template `a,b b,\"c,c\",\" d, d \"` is trimmed to `a,b b,"c,c","d, d"`
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b b,\"c,c\",\" d, d \"", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //                       'payload', { a: 1, "b b":2, "c,c":3, "d, d": 4 } // Legacy
+                        msg.should.have.property('payload', { a: 1, "b b": 2, "c,c": 3, " d, d ": 4 }); // RFC-vs-Legacy difference
+                        //                       'columns', 'a,b b,"c,c","d, d"' // Legacy
+                        msg.should.have.property('columns', 'a,b b,"c,c"," d, d "'); // RFC-vs-Legacy difference
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow passing in a template as first line of CSV', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, "b b": 2, "c,c": 3, " d, d ": 4 }); // RFC-vs-Legacy difference
+                        msg.should.have.property('columns', 'a,b b,"c,c"," d, d "'); // RFC-vs-Legacy difference
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = 'a,b b,"c,c"," d, d "' + "\n" + "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow passing in a template as first line of CSV (not comma)', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, sep: ";", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, "b b": 2, "c;c": 3, " d, d ": 4 }); // RFC-vs-Legacy difference
+                        msg.should.have.property('columns', 'a,b b,c;c," d, d "'); // RFC-vs-Legacy difference
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = 'a;b b;"c;c";" d, d "' + "\n" + "1;2;3;4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow passing in a template as first line of CSV (special char /)', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, sep: "/", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, "b b": 2, "c/c": 3, " d, d ": 4 }); // RFC-vs-Legacy difference
+                        msg.should.have.property('columns', 'a,b b,c/c," d, d "'); // RFC-vs-Legacy difference
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = 'a/b b/"c/c"/" d, d "' + "\n" + "1/2/3/4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow passing in a template as first line of CSV (special char \\)', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, spaces are respected in the template (RFC4180 Section 2.4)
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, sep: "\\", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, "b b": 2, "c\\c": 3, " d, d ": 4 }); // RFC-vs-Legacy difference
+                        msg.should.have.property('columns', 'a,b b,c\\c," d, d "'); // RFC-vs-Legacy difference
+                        check_parts(msg, 0, 1);
+                        done();
+                    } catch (e) { done(e); }
+                });
+                const testString = 'a\\b b\\"c\\c"\\" d, d "' + "\n" + "1\\2\\3\\4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should leave numbers starting with 0, e and + as strings (except 0.)', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 123, b: "0123", c: '+123', d: 'e123', e: 'E123', f: -123 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '123,0123,+123,e123,E123,-123' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should not parse numbers when told not to do so', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", strings: false, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: "1.23", b: "0123", c: "+123", d: "e123", e: "0", f: "-123", g: "1e3" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '1.23,0123,+123,e123,0,-123,1e3' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should parse numbers when told to do so', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1.23, b: -123, c: 1000, d: 0 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = ' 1.23 ,  -123,1e3 ,    0  ' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should leave handle strings with scientific notation as numbers', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 12000, b: 0.012, c: -12000, d: -0.012 });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '12E3,12e-3,-12e3,-12E-3' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+
+        it('should allow quotes in the input (but drop blank strings)', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g,h", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a: 1, b: -2, c: '+3', d: '04', f: '-05', g: 'ab"cd', h: 'with,a,comma' });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '"1","-2","+3","04","","-05","ab""cd","with,a,comma"' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow blank strings in the input if selected', async function () {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", include_empty_strings: true, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            await helper.load(csvNode, flow)
+            const n1 = helper.getNode("n1");
+            const n2 = helper.getNode("n2");
+            await new Promise((resolve, reject) => {
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: '', c: '', d: '', e: '-05', f: 'ab"cd', g: 'with,a,comma' });
+                        resolve()
+                    } catch (err) {
+                        reject(err);
+                    }
+                });
+                const testString = '"1","","","","-05","ab""cd","with,a,comma"' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should allow missing columns (nulls) in the input if selected', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", include_null_values: true, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a: 1, b: null, c: '+3', d: null, e: '-05', f: 'ab"cd', g: 'with,a,comma' });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '"1",,"+3",,"-05","ab""cd","with,a,comma"' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should handle cr and lf in the input', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //console.log(msg);
+                        msg.should.have.property('payload', { a: "with a\nnew line", b: "and a\rcarriage return", c: "and why\r\nnot both" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '"with a' + String.fromCharCode(10) + 'new line","and a' + String.fromCharCode(13) + 'carriage return","and why\r\nnot both"' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should recover from an odd number of quotes in the input', function (done) {
+            // RFC-vs-Legacy difference
+            // In RFC mode, the column template is parsed in strict mode
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let c = 0;
+                n2.on("input", function (msg) {
+                    try {
+                        if (c == 0) {
+                            c = 1;
+                            //                       'payload', { a: "with,an", b: "odd,number", c: "ofquotes\n" } // Legacy
+                            msg.should.have.property('payload', { a: 'with,a"n', b: 'odd', c: 'num"ber', d: 'of"qu"ot"es' }); // RFC-vs-Legacy difference
+                            // the result in this bad input data case as defined in RFC4180 would be to fail the parse (strict mode does this, but we don't enforce it in the data)
+                            // However, to better parse _acceptably_ bad data like a,b"b,c'c,"d,d" (which the newer RFC mode parser correctly parses as a,b"b,c'c,"d,d") we would 
+                            // need specific code to handle this case and that code would be on the hot path for all data.
+                            // This feels like an acceptable trade-off especially as the legacy mode parser is to be kept for backwards compatibility (until we can remove it)
+                            check_parts(msg, 0, 1);
+                        }
+                        else {
+                            msg.should.have.property('payload', { a: "this is", b: "a normal", c: "line" });
+                            check_parts(msg, 0, 1);
+                            done();
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = '"with,a"n,odd","num"ber","of"qu"ot"es"' + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+                n1.emit("input", { payload: '"this is","a normal","line"' });
+            });
+        });
+
+        it('should handle newlines in the input data', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e,f,g", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+
+                n2.on("input", function (msg) {
+                    try {
+                        //console.log(msg)
+                        // input => 'ay,be,"c has 2\nnew\nlines",dee,eee,eff,gee'
+                        msg.should.have.property('payload', { a: 'ay', b: 'be', c: 'c has 2\nnew\nlines', d: 'dee', e: 'eee', f: 'eff', g: 'gee' });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+
+                n1.emit("input", { payload: 'ay,be,"c has 2\nnew\nlines",dee,eee,eff,gee' });
+            });
+        });
+
+        it('should be able to use the first line as a template', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", hdrin: true, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let c = 0;
+                n2.on("input", function (msg) {
+                    try {
+                        //console.log(msg);
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "w,x,y,z\n1,2,3,4\n\n5,6,7,8";
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should be able to output multiple lines as one array', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", multi: "yes", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', [{ a: 1, b: 2, c: 3, d: 4 }, { a: 5, b: -6, c: '07', d: '+8' }, { a: 9, b: 0, c: 'a', d: 'b' }, { a: 'c', b: 'd', c: 'e', d: 'f' }]);
+                        msg.should.have.property('columns', 'a,b,c,d');
+                        msg.should.not.have.property('parts');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4\n5,-6,07,+8\n9,0,a,b\nc,d,e,f";
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should be able to create an array from multiple parts', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, multi: "mult", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', [{ "a": 1, "b": 2, "c": 3 }, { "a": 4, "b": 5, "c": 6 }, { "a": 7, "b": 8, "c": 9 }]);
+                        msg.should.have.property('columns', 'a,b,c');
+                        msg.should.not.have.property('parts');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+
+                n1.emit("input", { "payload": "a,b,c", "parts": { "index": 0, "ch": "\n", "type": "string", "id": "1" } });
+                n1.emit("input", { "payload": "1,2,3", "parts": { "index": 1, "ch": "\n", "type": "string", "id": "1" } });
+                n1.emit("input", { "payload": "4,5,6", "parts": { "index": 2, "ch": "\n", "type": "string", "id": "1" } });
+                n1.emit("input", { "payload": "7,8,9", "parts": { "index": 3, count: 4, "ch": "\n", "type": "string", "id": "1" } });
+            });
+        });
+
+        it('should be able to output multiple objects as an array from an input of parts', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, multi: "yes", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', [{ "Col1": "V1", "Col2": "V2" }, { "Col1": "V3", "Col2": "V4" }, { "Col1": "V5", "Col2": "V6" }]);
+                        msg.should.have.property('columns', 'Col1,Col2');
+                        msg.should.have.property('parts');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                //var testString = "1,2,3,4\n5,-6,07,+8\n9,0,a,b\nc,d,e,f";
+                // n1.emit("input", {payload:testString});
+                n1.emit("input", { "payload": "Col1,Col2\nV1,V2\nV3,V4\nV5,V6", "topic": "", "parts": { "id": "3af07e18.865652", "type": "array", "count": 2, "len": 1, "index": 0 } });
+                //n1.emit("input", {"payload":"Var1,Var2\nW1,W2\nW3,W4\nW5,W6","topic":"","parts":{"id":"3af07e18.865652","type":"array","count":2,"len":1,"index":1}});
+            });
+        });
+
+        it('should handle numbers in strings but not IP addresses', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d,e", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: "a", b: "127.0.0.1", c: 56.7, d: -32.8, e: "+76.22C" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "a,127.0.0.1,56.7,-32.8,+76.22C";
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should preserve parts property', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 1, b: 2, c: 3, d: 4 });
+                        check_parts(msg, 3, 4);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString, parts: { id: "X", index: 3, count: 4 } });
+            });
+        });
+
+        it('should be able to use the first of multiple parts as a template if parts are present', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let c = 0;
+                n2.on("input", function (msg) {
+                    try {
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString1 = "w,x,y,z\n";
+                const testString2 = "1,2,3,4\n";
+                const testString3 = "5,6,7,8\n";
+                n1.emit("input", { payload: testString1, parts: { id: "X", index: 0, count: 3 } });
+                n1.emit("input", { payload: testString2, parts: { id: "X", index: 1, count: 3 } });
+                n1.emit("input", { payload: testString3, parts: { id: "X", index: 2, count: 3 } });
+            });
+        });
+
+        it('should skip several lines from start if requested', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", skip: 2, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { a: 9, b: 0, c: "A", d: "B" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10) + "5,6,7,8" + String.fromCharCode(10) + "9,0,A,B" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should skip several lines from start then use next line as a template', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", hdrin: true, skip: 2, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', { "9": "C", "0": "D", "A": "E", "B": "F" });
+                        check_parts(msg, 0, 1);
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10) + "5,6,7,8" + String.fromCharCode(10) + "9,0,A,B" + String.fromCharCode(10) + "C,D,E,F" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should skip several lines from start and correct parts', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", skip: 2, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let c = 0;
+                n2.on("input", function (msg) {
+                    try {
+                        if (c === 0) {
+                            msg.should.have.property('payload', { a: 9, b: 0, c: "A", d: "B" });
+                            check_parts(msg, 0, 2);
+                            c = c + 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { a: "C", b: "D", c: "E", d: "F" });
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testString = "1,2,3,4" + String.fromCharCode(10) + "5,6,7,8" + String.fromCharCode(10) + "9,0,A,B" + String.fromCharCode(10) + "C,D,E,F" + String.fromCharCode(10);
+                n1.emit("input", { payload: testString });
+            });
+        });
+
+        it('should be able to skip and then use the first of multiple parts as a template if parts are present', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrin: true, skip: 2, wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let c = 0;
+                n2.on("input", function (msg) {
+                    try {
+                        if (c === 0) {
+                            msg.should.have.property('payload', { w: 1, x: 2, y: 3, z: 4 });
+                            msg.should.have.property('columns', 'w,x,y,z');
+                            check_parts(msg, 0, 2);
+                            c += 1;
+                        }
+                        else {
+                            msg.should.have.property('payload', { w: 5, x: 6, y: 7, z: 8 });
+                            msg.should.have.property('columns', 'w,x,y,z');
+                            check_parts(msg, 1, 2);
+                            done();
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testStringA = "foo\n";
+                const testStringB = "bar\n";
+                const testString1 = "w,x,y,z\n";
+                const testString2 = "1,2,3,4\n";
+                const testString3 = "5,6,7,8\n";
+                n1.emit("input", { payload: testStringA, parts: { id: "X", index: 0, count: 5 } });
+                n1.emit("input", { payload: testStringB, parts: { id: "X", index: 1, count: 5 } });
+                n1.emit("input", { payload: testString1, parts: { id: "X", index: 2, count: 5 } });
+                n1.emit("input", { payload: testString2, parts: { id: "X", index: 3, count: 5 } });
+                n1.emit("input", { payload: testString3, parts: { id: "X", index: 4, count: 5 } });
+            });
+        });
+
+    });
+
+    describe('json object to csv', function () {
+
+        it('should convert a simple object back to a csv', function (done) {
+            // RFC-vs-Legacy difference
+            // By default, the legacy mode parser will use \n as the line separator
+            // The RFC mode parser will use \r\n as the line separator as per RFC4180 Section 2.1
+            // This is configurable in both modes meaning that the legacy mode parser can be made to use \r\n and the RFC mode parser can be made to use \n
+            // Any existing flows will still use the line ending that they were created with as it will be stored in the flow file
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,,e,f,g,h,i,j,k", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    // console.log("GOT",msg)
+                    try {
+                        //                       'payload', '4,foo,true,,0,"Hello\nWorld",,,undefined,null,null\n'); // Legacy
+                        msg.should.have.property('payload', '4,foo,true,,0,"Hello\nWorld",,,undefined,null,null\r\n'); // RFC-vs-Legacy difference
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const 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 });
+            });
+        });
+
+        it('should convert a simple object back to a csv with no template', function (done) {
+            // RFC-vs-Legacy differences
+            // line separator handling:
+            //   By default, the legacy mode parser will use \n as the line separator
+            //   The RFC mode parser will use \r\n as the line separator as per RFC4180 Section 2.1
+            // spaces:
+            //   By default, the legacy mode parser will trim spaces from the start and end template columns
+            //   The RFC mode parser will not trim spaces from the start and end template columns (RFC4180 Section 2.4)
+            //   This means the original test flow, when ran in RFC mode, was expecting to find a prop name of " " in the input data object
+            // To make this test work, we need to change the column template to not be a space!
+
+            //  flow = [ { id:"n1", type:"csv", spec: "rfc", temp:" ", wires:[["n2"]] }, // Legacy
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", wires: [["n2"]] },  // RFC-vs-Legacy difference - legacy mode trims spaces, RFC mode respects them.
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    // console.log("GOT",msg)
+                    try {
+                        msg.should.have.property('payload', '1,foo,"ba""r","di,ng",,undefined,null\r\n');
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng", e: undefined, f: "undefined", g: null, h: "null" };
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert a simple object back to a tsv using a tab as a separator', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", sep: "\t", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', '1\tfoo\t"ba""r"\tdi,ng\n');
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const testJson = { d: 1, b: "foo", c: "ba\"r", a: "di,ng" };
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should handle a template with spaces in the property names', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b o,c p,,e", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', '4,foo,true,,0\n');
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const testJson = { e: 0, d: 1, "b o": "foo", "c p": true, a: 4 };
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should handle a template with quotes in the property names', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrout: "all", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //                       'payload', 'a"a,b\'b\nA1,B1\nA2,B2\n'); // Legacy
+                        msg.should.have.property('payload', '"a""a",b\'b\nA1,B1\nA2,B2\n'); // RFC-vs-Legacy difference - RFC4180 Section 2.6, 2.7 quote handling
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const 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) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,d,c,b", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', '4,1,2,3\n1,4,3,2\n');
+                        done();
+                    } catch (e) {
+                        done(e);
+                    }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }, { d: 4, a: 1, c: 3, b: 2 }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert an array of objects to a multi-line csv and add a header', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", hdrout: "all", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', 'a,b,c,d\n4,3,2,1\n1,2,3,"a\nb"\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }, { d: "a\nb", a: 1, c: 3, b: 2 }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert an array of objects to a multi-line csv without a template', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', '1,3,2,4\n4,2,3,1\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }, { d: 4, a: 1, c: 3, b: 2 }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert an array of objects to a multi-line csv without a template and with a header', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrout: "all", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', 'd,b,c,a\n1,3,2,4\n4,"f\ng",3,1\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }, { d: 4, a: 1, c: 3, b: "f\ng" }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert a simple array back to a csv', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //                       'payload', ',0,1,foo,"ba""r","di,ng","fa\nba"\n');
+                        msg.should.have.property('payload', ',0,1,foo\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = ["", 0, 1, "foo", 'ba"r', 'di,ng', "fa\nba"];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should convert an array of arrays back to a multi-line csv', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        //                       'payload', '0,1,2,3,4\n4,3,2,1,0\n'); // Legacy
+                        msg.should.have.property('payload', '0,1,2,3\n4,3,2,1\n'); // RFC-vs-Legacy difference - respect that user has specified a template with 4 columns
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [[0, 1, 2, 3, 4], [4, 3, 2, 1, 0]];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should be able to include column names as first row', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", hdrout: true, ret: "\r\n", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', 'a,b,c,d\r\n4,3,2,1\r\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+        it('should be able to include column names as first row, and missing properties', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", hdrout: true, ret: "\r\n", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', 'col1,col2,col3,col4\r\nH1,H2,H3,H4\r\nA,B,,\r\nA,,C,\r\nA,,,"D\nE"\r\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ "col1": "H1", "col2": "H2", "col3": "H3", "col4": "H4" }, { "col1": "A", "col2": "B" }, { "col1": "A", "col3": "C" }, { "col1": "A", "col4": "D\nE" }];
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+
+        it('should be able to pass in column names', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "", hdrout: "once", ret: "\r\n", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                let count = 0;
+                n2.on("input", function (msg) {
+                    count += 1;
+                    try {
+                        if (count === 1) {
+                            msg.should.have.property('payload', 'a,,b,a\r\n4,,3,4\r\n');
+                        }
+                        if (count === 3) {
+                            msg.should.have.property('payload', '4,,3,4\r\n');
+                            done()
+                        }
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = [{ d: 1, b: 3, c: 2, a: 4 }];
+                n1.emit("input", { payload: testJson, columns: "a,,b,a", parts: { index: 0 } });
+                n1.emit("input", { payload: testJson, parts: { index: 1 } });
+                n1.emit("input", { payload: testJson, parts: { index: 2 } });
+            });
+        });
+
+        it('should be able to pass in column names - with payload as an array', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", hdrout: "once", ret: "\r\n", wires: [["n2"]] },
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', 'a,,b,a\r\n4,,3,4\r\n4,,3,4\r\n4,,3,4\r\n');
+                        done()
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = { d: 1, b: 3, c: 2, a: 4 };
+                n1.emit("input", { payload: [testJson, testJson, testJson], columns: "a,,b,a" });
+            });
+        });
+
+        it('should handle quotes and sub-properties', function (done) {
+            const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", ret: '\n', wires: [["n2"]] }, // RFC-vs-Legacy difference - use line separator \n to satisfy original test
+            { id: "n2", type: "helper" }];
+            helper.load(csvNode, flow, function () {
+                const n1 = helper.getNode("n1");
+                const n2 = helper.getNode("n2");
+                n2.on("input", function (msg) {
+                    try {
+                        msg.should.have.property('payload', '{},"text,with,commas","This ""is"" a banana","{""sub"":""object""}"\n');
+                        done();
+                    }
+                    catch (e) { done(e); }
+                });
+                const testJson = { d: { sub: "object" }, b: "text,with,commas", c: 'This "is" a banana', a: { sub2: undefined } };
+                n1.emit("input", { payload: testJson });
+            });
+        });
+
+    });
+
+    it('should just pass through if no payload provided', function (done) {
+        const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [["n2"]] },
+        { id: "n2", type: "helper" }];
+        helper.load(csvNode, flow, function () {
+            const n1 = helper.getNode("n1");
+            const n2 = helper.getNode("n2");
+            n2.on("input", function (msg) {
+                try {
+                    msg.should.have.property('topic', { a: 4, b: 3, c: 2, d: 1 });
+                    msg.should.not.have.property('payload');
+
+                    done();
+                }
+                catch (e) { done(e); }
+            });
+            const testJson = { d: 1, b: 3, c: 2, a: 4 };
+            n1.emit("input", { topic: testJson });
+        });
+    });
+
+    it('should warn if provided a number or boolean', function (done) {
+        const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [["n2"]] },
+        { id: "n2", type: "helper" }];
+        helper.load(csvNode, flow, function () {
+            const n1 = helper.getNode("n1");
+            const n2 = helper.getNode("n2");
+            setTimeout(function () {
+                try {
+                    const logEvents = helper.log().args.filter(function (evt) {
+                        return evt[0].type == "csv";
+                    });
+                    logEvents.should.have.length(2);
+                    logEvents[0][0].should.have.a.property('msg');
+                    logEvents[0][0].msg.toString().should.endWith('csv.errors.csv_js');
+                    logEvents[1][0].should.have.a.property('msg');
+                    logEvents[1][0].msg.toString().should.endWith('csv.errors.csv_js');
+                    done();
+                } catch (err) {
+                    done(err);
+                }
+            }, 150);
+            n1.emit("input", { payload: 1 });
+            n1.emit("input", { payload: true });
+        });
+    });
+
+    it('should call done when message processing is completed', function (done) {
+        const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
+        const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [[]] },
+        { id: "c1", type: "complete", scope: ["n1"], uncaught: false, wires: [["h1"]] },
+        { id: "h1", type: "helper", wires: [[]] }];
+        helper.load([csvNode, completeNode], flow, function () {
+            const n1 = helper.getNode("n1");
+            const h1 = helper.getNode("h1");
+            h1.on("input", function (msg) {
+                try {
+                    msg.should.have.a.property('payload', "1,2,3,4");
+                    done();
+                } catch (e) {
+                    done(e);
+                }
+            });
+            n1.receive({ payload: "1,2,3,4" });
+        });
+    });
+
+    it('should not call done or pass the bad msg through when input causes an error - should throw error and set status', function (done) {
+        // RFC-vs-Legacy difference
+        // In RFC mode, instead of passing the bad data through, the node will throw an error and set the status to indicate the error
+        const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");
+        const flow = [{ id: "n1", type: "csv", spec: "rfc", temp: "a,b,c,d", wires: [[]] },
+        { id: "c1", type: "complete", scope: ["n1"], uncaught: false, wires: [["h1"]] },
+        { id: "h1", type: "helper", wires: [[]] }];
+        helper.load([csvNode, completeNode], flow, function () {
+            const n1 = helper.getNode("n1");
+            const h1 = helper.getNode("h1");
+            const c1 = helper.getNode("c1");
+            n1.receive({ payload: 1 }); // neither object nor string
+            setTimeout(function () {
+                try {
+                    c1.send.should.not.be.called();
+                    const logEvents = helper.log().args.filter(function (evt) {
+                        return evt[0].type == "csv";
+                    });
+                    logEvents.should.have.length(1);
+                    logEvents[0][0].should.have.a.property('msg');
+                    logEvents[0][0].msg.should.be.an.Error()
+                    logEvents[0][0].msg.toString().should.endWith('csv.errors.csv_js');
+                    // check status was set
+                    n1.status.called.should.be.true();
+                    n1.status.lastCall.args[0].should.have.property('fill', 'red');
+                    n1.status.lastCall.args[0].should.have.property('shape', 'dot');
+                    n1.status.lastCall.args[0].should.have.property('text', 'csv.errors.csv_js');
+                    // check error was thrown
+                    n1.error.called.should.be.true();
+                    n1.error.lastCall.args[0].should.have.property('message', 'csv.errors.csv_js');
+                    done();
+                } catch (err) {
+                    done(err);
+                }
+            }, 150);
+
+        });
+    });
+});
diff --git a/test/nodes/core/sequence/17-split_spec.js b/test/nodes/core/sequence/17-split_spec.js
index 370e0cda4..a64f6e078 100644
--- a/test/nodes/core/sequence/17-split_spec.js
+++ b/test/nodes/core/sequence/17-split_spec.js
@@ -66,6 +66,27 @@ describe('SPLIT node', function() {
         });
     });
 
+    it('should split an array on a sub-property into multiple messages', function(done) {
+        var flow = [{id:"sn1", type:"split", property:"foo", wires:[["sn2"]]},
+                    {id:"sn2", type:"helper"}];
+        helper.load(splitNode, flow, function() {
+            var sn1 = helper.getNode("sn1");
+            var sn2 = helper.getNode("sn2");
+            sn2.on("input", function(msg) {
+                msg.should.have.property("parts");
+                msg.parts.should.have.property("count",4);
+                msg.parts.should.have.property("type","array");
+                msg.parts.should.have.property("index");
+                msg.parts.should.have.property("property","foo");
+                if (msg.parts.index === 0) { msg.foo.should.equal(1); }
+                if (msg.parts.index === 1) { msg.foo.should.equal(2); }
+                if (msg.parts.index === 2) { msg.foo.should.equal(3); }
+                if (msg.parts.index === 3) { msg.foo.should.equal(4); done(); }
+            });
+            sn1.receive({foo:[1,2,3,4]});
+        });
+    });
+
     it('should split an array into multiple messages of a specified size', function(done) {
         var flow = [{id:"sn1", type:"split", wires:[["sn2"]], arraySplt:3, arraySpltType:"len"},
                     {id:"sn2", type:"helper"}];
@@ -108,6 +129,31 @@ describe('SPLIT node', function() {
         });
     });
 
+    it('should split an object sub property into pieces', function(done) {
+        var flow = [{id:"sn1", type:"split", property:"foo.bar",wires:[["sn2"]]},
+                    {id:"sn2", type:"helper"}];
+        helper.load(splitNode, flow, function() {
+            var sn1 = helper.getNode("sn1");
+            var sn2 = helper.getNode("sn2");
+            var count = 0;
+            sn2.on("input", function(msg) {
+                msg.should.have.property("foo");
+                msg.foo.should.have.property("bar");
+                msg.should.have.property("parts");
+                msg.parts.should.have.property("type","object");
+                msg.parts.should.have.property("key");
+                msg.parts.should.have.property("count");
+                msg.parts.should.have.property("index");
+                msg.parts.should.have.property("property","foo.bar");
+                msg.topic.should.equal("foo");
+                if (msg.parts.index === 0) { msg.foo.bar.should.equal(1); }
+                if (msg.parts.index === 1) { msg.foo.bar.should.equal("2"); }
+                if (msg.parts.index === 2) { msg.foo.bar.should.equal(true); done(); }
+            });
+            sn1.receive({topic:"foo",foo:{bar:{a:1,b:"2",c:true}}});
+        });
+    });
+
     it('should split an object into pieces and overwrite their topics', function(done) {
         var flow = [{id:"sn1", type:"split", addname:"topic", wires:[["sn2"]]},
                     {id:"sn2", type:"helper"}];
@@ -516,6 +562,7 @@ describe('JOIN node', function() {
             n1.receive({payload:{a:1}});
         });
     });
+
     it('should join things into an array ignoring msg.parts.index in manual mode', function(done) {
         var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",",mode:"custom"},
                     {id:"n2", type:"helper"}];
@@ -562,6 +609,32 @@ describe('JOIN node', function() {
         });
     });
 
+    it('should join things into an array on a sub property in auto mode', function(done) {
+        var flow = [{id:"n1", type:"join", wires:[["n2"]], count:3, joiner:",", mode:"auto"},
+                    {id:"n2", type:"helper"}];
+        helper.load(joinNode, flow, function() {
+            var n1 = helper.getNode("n1");
+            var n2 = helper.getNode("n2");
+            n2.on("input", function(msg) {
+                try {
+                    msg.should.have.property("foo");
+                    msg.foo.should.have.property("bar");
+                    msg.foo.bar.should.be.an.Array();
+                    msg.foo.bar[0].should.equal("A");
+                    msg.foo.bar[1].should.equal("B");
+                    //msg.payload[2].a.should.equal(1);
+                    done();
+                }
+                catch(e) {done(e);}
+            });
+            n1.receive({foo:{bar:"A"}, parts:{id:1, type:"array", len:1, index:0, count:4, property:"foo.bar"}});
+            n1.receive({foo:{bar:"B"}, parts:{id:1, type:"array", len:1, index:1, count:4, property:"foo.bar"}});
+            n1.receive({foo:{bar:"C"}, parts:{id:1, type:"array", len:1, index:2, count:4, property:"foo.bar"}});
+            n1.receive({foo:{bar:"D"}, parts:{id:1, type:"array", len:1, index:3, count:4, property:"foo.bar"}});
+        });
+    });
+
+
     it('should join strings into a buffer after a count', function(done) {
         var flow = [{id:"n1", type:"join", wires:[["n2"]], count:2, build:"buffer", joinerType:"bin", joiner:"", mode:"custom"},
                     {id:"n2", type:"helper"}];
@@ -639,6 +712,35 @@ describe('JOIN node', function() {
         });
     });
 
+    it('should merge sub property objects', function(done) {
+        var flow = [{id:"n1", type:"join", wires:[["n2"]], count:5, property:"foo.bar", build:"merged", mode:"custom"},
+                    {id:"n2", type:"helper"}];
+        helper.load(joinNode, flow, function() {
+            var n1 = helper.getNode("n1");
+            var n2 = helper.getNode("n2");
+            n2.on("input", function(msg) {
+                try {
+                    msg.should.have.property("foo");
+                    msg.foo.should.have.property("bar");
+                    msg.foo.bar.should.have.property("a",1);
+                    msg.foo.bar.should.have.property("b",2);
+                    msg.foo.bar.should.have.property("c",3);
+                    msg.foo.bar.should.have.property("d",4);
+                    msg.foo.bar.should.have.property("e",5);
+                    done();
+                }
+                catch(e) { done(e)}
+            });
+            n1.receive({foo:{bar:{a:9}, topic:"f"}});
+            n1.receive({foo:{bar:{a:1}, topic:"a"}});
+            n1.receive({foo:{bar:{b:9}, topic:"b"}});
+            n1.receive({foo:{bar:{b:2}, topic:"b"}});
+            n1.receive({foo:{bar:{c:3}, topic:"c"}});
+            n1.receive({foo:{bar:{d:4}, topic:"d"}});
+            n1.receive({foo:{bar:{e:5}, topic:"e"}});
+        });
+    });
+
     it('should merge full msg objects', function(done) {
         var flow = [{id:"n1", type:"join", wires:[["n2"]], count:6, build:"merged", mode:"custom", propertyType:"full", property:""},
                     {id:"n2", type:"helper"}];
diff --git a/test/unit/@node-red/registry/lib/library_spec.js b/test/unit/@node-red/registry/lib/library_spec.js
index 2e0e7e99a..75c444f67 100644
--- a/test/unit/@node-red/registry/lib/library_spec.js
+++ b/test/unit/@node-red/registry/lib/library_spec.js
@@ -33,16 +33,15 @@ describe("library api", function() {
         should.not.exist(library.getExampleFlowPath('foo','bar'));
     });
 
-    it('returns a valid example path', function(done) {
+    it('returns valid example paths', function(done) {
         library.init();
         library.addExamplesDir("test-module",path.resolve(__dirname+'/resources/examples')).then(function() {
             try {
                 var flows = library.getExampleFlows();
-                flows.should.deepEqual({"test-module":{"f":["one"]}});
+                flows.should.deepEqual({"test-module":{"f":["1.2.3","one"]}});
 
                 var examplePath = library.getExampleFlowPath('test-module','one');
-                examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'))
-
+                examplePath.should.eql(path.resolve(__dirname+'/resources/examples/one.json'));
 
                 library.removeExamplesDir('test-module');
 
@@ -57,6 +56,5 @@ describe("library api", function() {
                 done(err);
             }
         });
-
-    })
+    });
 });
diff --git a/test/unit/@node-red/registry/lib/resources/examples/1.2.3.json b/test/unit/@node-red/registry/lib/resources/examples/1.2.3.json
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js
index 3bab2739a..e48e9fc84 100644
--- a/test/unit/@node-red/util/lib/util_spec.js
+++ b/test/unit/@node-red/util/lib/util_spec.js
@@ -379,10 +379,17 @@ describe("@node-red/util/util", function() {
             result = util.evaluateNodeProperty('','bool');
             result.should.be.false();
         });
-        it('returns date',function() {
+        it('returns date - default format',function() {
             var result = util.evaluateNodeProperty('','date');
             (Date.now() - result).should.be.approximately(0,50);
         });
+
+        it('returns date - iso format',function() {
+            var result = util.evaluateNodeProperty('iso','date');
+            // 2023-12-04T16:51:04.429Z
+            /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$/.test(result).should.be.true()
+        });
+
         it('returns bin', function () {
             var result = util.evaluateNodeProperty('[1, 2]','bin');
             result[0].should.eql(1);
@@ -441,9 +448,16 @@ describe("@node-red/util/util", function() {
             },{});
             result.should.eql("123");
         });
-        it('returns jsonata result', function () {
-            var result = util.evaluateNodeProperty('$abs(-1)','jsonata',{},{});
-            result.should.eql(1);
+        it('returns jsonata result', function (done) {
+            util.evaluateNodeProperty('$abs(-1)','jsonata',{},{}, (err, result) => {
+                try {
+                    result.should.eql(1);
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+
+            });
         });
         it('returns null', function() {
             var result = util.evaluateNodeProperty(null,'null');
@@ -601,51 +615,105 @@ describe("@node-red/util/util", function() {
           });
       });
       describe('evaluateJSONataExpression', function() {
-          it('evaluates an expression', function() {
+          it('evaluates an expression', function(done) {
               var expr = util.prepareJSONataExpression('payload',{});
-              var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
-              result.should.eql("hello");
+              util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
+                try {
+                    result.should.eql("hello");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('evaluates a legacyMode expression', function() {
               var expr = util.prepareJSONataExpression('msg.payload',{});
-              var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
-              result.should.eql("hello");
+              util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
+                try {
+                    result.should.eql("hello");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('accesses flow context from an expression', function() {
               var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
-              var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
-              result.should.eql("bar");
+              util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
+                try {
+                    result.should.eql("bar");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('accesses undefined environment variable from an expression', function() {
               var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
-              var result = util.evaluateJSONataExpression(expr,{});
-              result.should.eql('');
-          });
+              util.evaluateJSONataExpression(expr,{}, (err, result) => {
+                try {
+                    result.should.eql("");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
+            });
           it('accesses environment variable from an expression', function() {
               process.env.UTIL_ENV = 'foo';
               var expr = util.prepareJSONataExpression('$env("UTIL_ENV")',{});
-              var result = util.evaluateJSONataExpression(expr,{});
-              result.should.eql('foo');
-          });
+              util.evaluateJSONataExpression(expr,{}, (err, result) => {
+                try {
+                    result.should.eql("foo");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
+            });
           it('accesses moment from an expression', function() {
               var expr = util.prepareJSONataExpression('$moment("2020-05-27", "YYYY-MM-DD").add(7, "days").add(1, "months").format("YYYY-MM-DD")',{});
-              var result = util.evaluateJSONataExpression(expr,{});
-              result.should.eql('2020-07-03');
+              util.evaluateJSONataExpression(expr,{}, (err, result) => {
+                try {
+                    result.should.eql("2020-07-03");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('accesses moment-timezone from an expression', function() {
               var expr = util.prepareJSONataExpression('$moment("2013-11-18 11:55Z").tz("Asia/Taipei").format()',{});
-              var result = util.evaluateJSONataExpression(expr,{});
-              result.should.eql('2013-11-18T19:55:00+08:00');
+              util.evaluateJSONataExpression(expr,{}, (err, result) => {
+                try {
+                    result.should.eql("2013-11-18T19:55:00+08:00");
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('handles non-existant flow context variable', function() {
               var expr = util.prepareJSONataExpression('$flowContext("nonExistant")',{context:function() { return {flow:{get: function(key) { return {'foo':'bar'}[key]}}}}});
-              var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
-              should.not.exist(result);
-          });
+              util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
+                try {
+                    should.not.exist(result);
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
+            });
           it('handles non-existant global context variable', function() {
               var expr = util.prepareJSONataExpression('$globalContext("nonExistant")',{context:function() { return {global:{get: function(key) { return {'foo':'bar'}[key]}}}}});
-              var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
-              should.not.exist(result);
+              util.evaluateJSONataExpression(expr,{payload:"hello"}, (err, result) => {
+                try {
+                    should.not.exist(result);
+                    done()
+                } catch (error) {
+                    done(error)
+                }
+              });
           });
           it('handles async flow context access', function(done) {
               var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,store,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});