mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge branch 'dev' into config-in-subflow
This commit is contained in:
		
							
								
								
									
										23
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								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 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -15,8 +15,6 @@ | ||||
|  **/ | ||||
|  | ||||
| var apiUtils = require("../util"); | ||||
| var fs = require('fs'); | ||||
| var fspath = require('path'); | ||||
|  | ||||
| var runtimeAPI; | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,6 @@ | ||||
|  **/ | ||||
|  | ||||
| var apiUtils = require("../util"); | ||||
| var express = require("express"); | ||||
| var runtimeAPI; | ||||
| var settings; | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|  * limitations under the License. | ||||
|  **/ | ||||
|  | ||||
| var express = require("express"); | ||||
| var util = require("util"); | ||||
| var path = require("path"); | ||||
| var fs = require("fs"); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -926,6 +926,12 @@ | ||||
|             "env": "env variable", | ||||
|             "cred": "credential", | ||||
|             "conf-types": "config node" | ||||
|         }, | ||||
|         "date": { | ||||
|             "format": { | ||||
|                 "timestamp": "milliseconds since epoch", | ||||
|                 "object": "JavaScript Date Object" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "editableList": { | ||||
|   | ||||
| @@ -303,7 +303,8 @@ | ||||
|                 "missingType": "不正なフロー - __index__ 番目の要素に'type'プロパティがありません" | ||||
|             }, | ||||
|             "conflictNotification1": "読み込もうとしているノードのいくつかは、既にワークスペース内に存在しています。", | ||||
|             "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。" | ||||
|             "conflictNotification2": "読み込むノードを選択し、また既存のノードを置き換えるか、もしくはそれらのコピーを読み込むかも選択してください。", | ||||
|             "alreadyExists": "本ノードは既に存在" | ||||
|         }, | ||||
|         "copyMessagePath": "パスをコピーしました", | ||||
|         "copyMessageValue": "値をコピーしました", | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -483,6 +483,16 @@ RED.utils = (function() { | ||||
|                 $('<span class="red-ui-debug-msg-type-string-swatch"></span>').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 = $('<span class="red-ui-debug-msg-type-number"></span>').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 { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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", { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										17
									
								
								packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/node_modules/@node-red/editor-client/src/sass/sizes.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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; | ||||
| @@ -15,4 +15,5 @@ | ||||
| **/ | ||||
|  | ||||
| @import "colors"; | ||||
| @import "sizes"; | ||||
| @import "variables"; | ||||
| @@ -15,6 +15,7 @@ | ||||
| **/ | ||||
|  | ||||
| @import "colors"; | ||||
| @import "sizes"; | ||||
| @import "variables"; | ||||
| @import "mixins"; | ||||
|  | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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<v.length;i++) { | ||||
|                         if (/^\${[^}]+}$/.test(v[i].v)) { | ||||
|                             // Allow ${ENV_VAR} value | ||||
|                             continue | ||||
|                         } | ||||
|                         if (/msg|flow|global/.test(v[i].vt)) { | ||||
|                             if (!RED.utils.validatePropertyExpression(v[i].v)) { | ||||
|                                 return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }); | ||||
|                                 errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v })) | ||||
|                             } | ||||
|                         } else if (v[i].vt === "jsonata") { | ||||
|                             try{ jsonata(v[i].v); } | ||||
|                             catch(e){ | ||||
|                                 return RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message }); | ||||
|                                 errors.push(RED._("node-red:inject.errors.invalid-jsonata", { prop: 'msg.'+v[i].p, error: e.message })) | ||||
|                             } | ||||
|                         } else if (v[i].vt === "json") { | ||||
|                             try{ JSON.parse(v[i].v); } | ||||
|                             catch(e){ | ||||
|                                 return RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message }); | ||||
|                                 errors.push(RED._("node-red:inject.errors.invalid-json", { prop: 'msg.'+v[i].p, error: e.message })) | ||||
|                             } | ||||
|                         } else if (v[i].vt === "num"){ | ||||
|                             if (!/^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$/.test(v[i].v)) { | ||||
|                                 return RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v }); | ||||
|                                 errors.push(RED._("node-red:inject.errors.invalid-prop", { prop: 'msg.'+v[i].p, error: v[i].v })) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     if (errors.length > 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"}, | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -17,7 +17,20 @@ | ||||
|            </select> | ||||
|            <input style="width:40px;" type="text" id="node-input-sep" pattern="."> | ||||
|     </div> | ||||
|  | ||||
|     <div class="form-row"> | ||||
|         <label><i class="fa fa-code"></i> <span data-i18n="csv.label.spec"></span></label> | ||||
|         <div style="display: inline-grid;width: 70%;"> | ||||
|             <select style="width:100%" id="csv-option-spec"> | ||||
|                 <option value="rfc" data-i18n="csv.spec.rfc"></option> | ||||
|                 <option value="" data-i18n="csv.spec.legacy"></option> | ||||
|             </select> | ||||
|             <div> | ||||
|                 <div class="form-tips csv-lecacy-warning" data-i18n="node-red:csv.spec.legacy_warning"  | ||||
|                     style="width: calc(100% - 18px); margin-top: 4px; max-width: unset;"> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> | ||||
|         <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"> | ||||
| @@ -60,10 +73,10 @@ | ||||
|     <div class="form-row" style="padding-left:20px;"> | ||||
|         <label></label> | ||||
|         <label style="width:auto; margin-right:10px;" for="node-input-ret"><span data-i18n="csv.label.newline"></span></label> | ||||
|         <select style="width:150px;" id="node-input-ret"> | ||||
|         <select style="width:calc(70% - 108px);" id="node-input-ret"> | ||||
|             <option value='\r\n' data-i18n="csv.newline.windows"></option> | ||||
|             <option value='\n' data-i18n="csv.newline.linux"></option> | ||||
|             <option value='\r' data-i18n="csv.newline.mac"></option> | ||||
|             <option value='\r\n' data-i18n="csv.newline.windows"></option> | ||||
|        </select> | ||||
|     </div> | ||||
| </script> | ||||
| @@ -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 | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|             <option value="html" data-i18n="html.output.html"></option> | ||||
|             <option value="text" data-i18n="html.output.text"></option> | ||||
|             <option value="attr" data-i18n="html.output.attr"></option> | ||||
|             <option value="compl" data-i18n="html.output.compl"></option> | ||||
|             <!-- <option value="val">return the value from a form element</option> --> | ||||
|         </select> | ||||
|     </div> | ||||
| @@ -28,6 +29,10 @@ | ||||
|         <label for="node-input-outproperty"> </label> | ||||
|         <span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%"> | ||||
|     </div> | ||||
|     <div id='html-prefix-row' class="form-row" style="display: none;"> | ||||
|         <label for="node-input-chr" style="width: 230px;"><i class="fa fa-tag"></i> <span data-i18n="html.label.prefix"></span></label> | ||||
|         <input type="text" id="node-input-chr" style="text-align:center; width: 40px;" placeholder="_"> | ||||
|     </div> | ||||
|     <br/> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> | ||||
| @@ -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() | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
|   | ||||
| @@ -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++; | ||||
|   | ||||
							
								
								
									
										324
									
								
								packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								packages/node_modules/@node-red/nodes/core/parsers/lib/csv/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -15,7 +15,11 @@ | ||||
| --> | ||||
|  | ||||
| <script type="text/html" data-template-name="split"> | ||||
|     <div class="form-row"><span data-i18n="[html]split.intro"></span></div> | ||||
|     <!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> --> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label> | ||||
|         <input type="text" id="node-input-property" style="width:70%;"/> | ||||
|     </div> | ||||
|     <div class="form-row"><span data-i18n="[html]split.strBuff"></span></div> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label> | ||||
| @@ -39,10 +43,9 @@ | ||||
|         <label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label> | ||||
|         <input type="text" id="node-input-addname" style="width:70%"> | ||||
|     </div> | ||||
|     <hr/> | ||||
|     <div class="form-row"> | ||||
|         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label> | ||||
|         <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"> | ||||
|         <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label> | ||||
|         <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name"> | ||||
|     </div> | ||||
| </script> | ||||
|  | ||||
| @@ -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"), | ||||
|   | ||||
| @@ -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<count-1; i++) { | ||||
|                             msg.payload = data.substring(pos,pos+node.splt); | ||||
|                             RED.util.setMessageProperty(msg,node.property,data.substring(pos,pos+node.splt)); | ||||
|                             msg.parts.index = node.c++; | ||||
|                             pos += node.splt; | ||||
|                             send(RED.util.cloneMessage(msg)); | ||||
| @@ -102,7 +109,7 @@ module.exports = function(RED) { | ||||
|                         } | ||||
|                         node.remainder = data.substring(pos); | ||||
|                         if ((node.stream !== true) || (node.remainder.length === node.splt)) { | ||||
|                             msg.payload = node.remainder; | ||||
|                             RED.util.setMessageProperty(msg,node.property,node.remainder); | ||||
|                             msg.parts.index = node.c++; | ||||
|                             send(RED.util.cloneMessage(msg)); | ||||
|                             node.pendingDones.forEach(d => 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<count; i++) { | ||||
|                         msg.payload = data.slice(pos,pos+node.arraySplt); | ||||
|                         var m = data.slice(pos,pos+node.arraySplt); | ||||
|                         if (node.arraySplt === 1) { | ||||
|                             msg.payload = msg.payload[0]; | ||||
|                             m = m[0]; | ||||
|                         } | ||||
|                         RED.util.setMessageProperty(msg,node.property,m); | ||||
|                         msg.parts.index = i; | ||||
|                         pos += node.arraySplt; | ||||
|                         send(RED.util.cloneMessage(msg)); | ||||
|                     } | ||||
|                     done(); | ||||
|                 } | ||||
|                 else if ((typeof msg.payload === "object") && !Buffer.isBuffer(msg.payload)) { | ||||
|                 else if ((typeof value === "object") && !Buffer.isBuffer(value)) { | ||||
|                     var j = 0; | ||||
|                     var l = Object.keys(msg.payload).length; | ||||
|                     var pay = msg.payload; | ||||
|                     var l = Object.keys(value).length; | ||||
|                     var pay = value; | ||||
|                     msg.parts.type = "object"; | ||||
|                     for (var p in pay) { | ||||
|                         if (pay.hasOwnProperty(p)) { | ||||
|                             msg.payload = pay[p]; | ||||
|                             RED.util.setMessageProperty(msg,node.property,pay[p]); | ||||
|                             if (node.addname !== "") { | ||||
|                                 msg[node.addname] = p; | ||||
|                                 RED.util.setMessageProperty(msg,node.addname,p); | ||||
|                             } | ||||
|                             msg.parts.key = p; | ||||
|                             msg.parts.index = j; | ||||
| @@ -170,9 +178,9 @@ module.exports = function(RED) { | ||||
|                     } | ||||
|                     done(); | ||||
|                 } | ||||
|                 else if (Buffer.isBuffer(msg.payload)) { | ||||
|                     var len = node.buffer.length + msg.payload.length; | ||||
|                     var buff = Buffer.concat([node.buffer, msg.payload], len); | ||||
|                 else if (Buffer.isBuffer(value)) { | ||||
|                     var len = node.buffer.length + value.length; | ||||
|                     var buff = Buffer.concat([node.buffer, value], len); | ||||
|                     msg.parts.type = "buffer"; | ||||
|                     if (node.spltType === "len") { | ||||
|                         var count = buff.length/node.splt; | ||||
| @@ -186,7 +194,7 @@ module.exports = function(RED) { | ||||
|                         var pos = 0; | ||||
|                         msg.parts.len = node.splt; | ||||
|                         for (var i=0; i<count-1; i++) { | ||||
|                             msg.payload = buff.slice(pos,pos+node.splt); | ||||
|                             RED.util.setMessageProperty(msg,node.property,buff.slice(pos,pos+node.splt)); | ||||
|                             msg.parts.index = node.c++; | ||||
|                             pos += node.splt; | ||||
|                             send(RED.util.cloneMessage(msg)); | ||||
| @@ -197,7 +205,7 @@ module.exports = function(RED) { | ||||
|                         } | ||||
|                         node.buffer = buff.slice(pos); | ||||
|                         if ((node.stream !== true) || (node.buffer.length === node.splt)) { | ||||
|                             msg.payload = node.buffer; | ||||
|                             RED.util.setMessageProperty(msg,node.property,node.buffer); | ||||
|                             msg.parts.index = node.c++; | ||||
|                             send(RED.util.cloneMessage(msg)); | ||||
|                             node.pendingDones.forEach(d => 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); | ||||
|  | ||||
|   | ||||
| @@ -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 <code>msg.payload</code> based on type:", | ||||
|         "object": "<b>Object</b>", | ||||
|         "objectSend": "Send a message for each key/value pair", | ||||
|   | ||||
| @@ -36,7 +36,9 @@ | ||||
|     </dl> | ||||
|     <h3>Details</h3> | ||||
|     <p>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.</p> | ||||
|     will be used as the property names. Alternatively, the column names can be taken from the first row of the CSV. | ||||
|         <p>When the RFC parser is selected, the column template must be compliant with RFC4180.</p> | ||||
|     </p> | ||||
|     <p>When converting to CSV, the columns template is used to identify which properties to extract from the object and in what order.</p> | ||||
|     <p>If the columns template is blank then you can use a simple comma separated list of properties supplied in <code>msg.columns</code> 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 @@ | ||||
|     <p>If outputting multiple messages they will have their <code>parts</code> property set and form a complete message sequence.</p> | ||||
|     <p>If the node is set to only send column headers once, then setting <code>msg.reset</code> to any value will cause the node to resend the headers.</p> | ||||
|     <p><b>Note:</b> the column template must be comma separated - even if a different separator is chosen for the data.</p> | ||||
|     <p><b>Note:</b> in RFC mode, catchable errors will be thrown for malformed CSV headers and invalid input payload data</p> | ||||
| </script> | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|         <dt class="optional">template <span class="property-type">string</span></dt> | ||||
|         <dd>由<code>msg.payload</code>填充的模板。如果未在编辑面板中配置,则可以将设为msg的属性。</dd> | ||||
|     </dl> | ||||
|     <h3>Outputs</h3> | ||||
|     <h3>输出</h3> | ||||
|     <dl class="message-properties"> | ||||
|         <dt>msg <span class="property-type">object</span></dt> | ||||
|         <dd>由来自传入msg的属性来填充已配置的模板后输出的带有属性的msg。</dd> | ||||
| @@ -32,7 +32,7 @@ | ||||
|     <p>默认情况下使用<i><a href="http://mustache.github.io/mustache.5.html" target="_blank">mustache</a></i>格式。如有需要也可以切换其他格式。</p> | ||||
|     <p>例如: | ||||
|     <pre>Hello {{payload.name}}. Today is {{date}}</pre> | ||||
|     <p>receives a message containing: | ||||
|     <p>接收一条消息,其中包含: | ||||
|     <pre>{ | ||||
|   date: "Monday", | ||||
|   payload: { | ||||
|   | ||||
| @@ -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$/, ''))) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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"); | ||||
|                 }) | ||||
|             }); | ||||
|         }, | ||||
|   | ||||
							
								
								
									
										23
									
								
								packages/node_modules/@node-red/util/lib/util.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								packages/node_modules/@node-red/util/lib/util.js
									
									
									
									
										vendored
									
									
								
							| @@ -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) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
							
								
								
									
										8
									
								
								packages/node_modules/node-red/red.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								packages/node_modules/node-red/red.js
									
									
									
									
										vendored
									
									
								
							| @@ -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); | ||||
|     } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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"}]; | ||||
|   | ||||
| @@ -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); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     }) | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -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)}}}}}); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user