Resolve merge conflicts on update from upstream

This commit is contained in:
andrew.greene 2022-01-20 16:39:43 -07:00
commit 965d877b63
62 changed files with 8263 additions and 121 deletions

3196
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/andrewsgreene/not-node-red.git" "url": "https://github.com/defenseunicorns/Sparkles-Guide.git"
}, },
"private": "true", "private": "true",
"scripts": { "scripts": {
@ -19,13 +19,13 @@
"docs": "grunt docs" "docs": "grunt docs"
}, },
"dependencies": { "dependencies": {
"acorn": "8.6.0", "acorn": "8.7.0",
"acorn-walk": "8.2.0", "acorn-walk": "8.2.0",
"ajv": "8.8.2", "ajv": "8.8.2",
"async-mutex": "0.3.2", "async-mutex": "0.3.2",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.19.0", "body-parser": "1.19.1",
"cheerio": "1.0.0-rc.10", "cheerio": "1.0.0-rc.10",
"clone": "2.1.2", "clone": "2.1.2",
"content-type": "1.0.4", "content-type": "1.0.4",
@ -34,7 +34,7 @@
"cors": "2.8.5", "cors": "2.8.5",
"cronosjs": "1.7.1", "cronosjs": "1.7.1",
"denque": "2.0.1", "denque": "2.0.1",
"express": "4.17.1", "express": "4.17.2",
"express-session": "1.17.2", "express-session": "1.17.2",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
@ -43,7 +43,7 @@
"hash-sum": "2.0.0", "hash-sum": "2.0.0",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"i18next": "21.5.4", "i18next": "21.6.6",
"iconv-lite": "0.6.3", "iconv-lite": "0.6.3",
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "3.14.1", "js-yaml": "3.14.1",
@ -54,21 +54,21 @@
"memorystore": "1.6.6", "memorystore": "1.6.6",
"mime": "2.5.2", "mime": "2.5.2",
"moment-timezone": "0.5.34", "moment-timezone": "0.5.34",
"mqtt": "4.2.8", "mqtt": "4.3.4",
"multer": "1.4.3", "multer": "1.4.3",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-red-admin": "^2.2.1", "node-red-admin": "^2.2.1",
"nopt": "5.0.0", "nopt": "5.0.0",
"oauth2orize": "1.11.1", "oauth2orize": "1.11.1",
"on-headers": "1.0.2", "on-headers": "1.0.2",
"passport": "0.5.0", "passport": "0.5.2",
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"raw-body": "2.4.2", "raw-body": "2.4.2",
"semver": "7.3.5", "semver": "7.3.5",
"tar": "6.1.11", "tar": "6.1.11",
"tough-cookie": "4.0.0", "tough-cookie": "4.0.0",
"uglify-js": "3.14.4", "uglify-js": "3.14.5",
"uuid": "8.3.2", "uuid": "8.3.2",
"ws": "7.5.1", "ws": "7.5.1",
"xml2js": "0.4.23" "xml2js": "0.4.23"
@ -78,7 +78,7 @@
}, },
"devDependencies": { "devDependencies": {
"cypress": "^9.1.1", "cypress": "^9.1.1",
"dompurify": "2.3.3", "dompurify": "2.3.4",
"grunt": "1.4.1", "grunt": "1.4.1",
"grunt-chmod": "~1.1.1", "grunt-chmod": "~1.1.1",
"grunt-cli": "~1.4.3", "grunt-cli": "~1.4.3",
@ -101,17 +101,17 @@
"i18next-http-backend": "1.3.1", "i18next-http-backend": "1.3.1",
"jquery-i18next": "1.2.1", "jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "3.0.7", "marked": "4.0.10",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "9.1.3", "mocha": "9.1.3",
"node-red-node-test-helper": "^0.2.7", "node-red-node-test-helper": "^0.2.7",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"proxy": "^1.0.2", "proxy": "^1.0.2",
"sass": "1.44.0", "sass": "1.48.0",
"should": "13.2.3", "should": "13.2.3",
"sinon": "11.1.2", "sinon": "11.1.2",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"supertest": "6.1.6" "supertest": "6.2.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"

View File

@ -146,7 +146,7 @@ function authenticateUserToken(req) {
} else { } else {
reject(); reject();
} }
}); }).catch(reject);
} else { } else {
reject(); reject();
} }
@ -163,6 +163,9 @@ TokensStrategy.prototype.authenticate = function(req) {
authenticateUserToken(req).then(user => { authenticateUserToken(req).then(user => {
this.success(user,{scope:user.permissions}); this.success(user,{scope:user.permissions});
}).catch(err => { }).catch(err => {
if (err) {
log.trace("token authentication failure: "+err.stack?err.stack:err)
}
this.fail(401); this.fail(401);
}); });
} }

View File

@ -90,6 +90,8 @@ function init(settings,_server,storage,runtimeAPI) {
auth.getToken, auth.getToken,
auth.errorHandler auth.errorHandler
); );
} else if (settings.adminAuth.tokens) {
adminApp.use(passport.initialize());
} }
adminApp.post("/auth/revoke",auth.needsPermission(""),auth.revoke,apiUtil.errorHandler); adminApp.post("/auth/revoke",auth.needsPermission(""),auth.revoke,apiUtil.errorHandler);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-api", "name": "@node-red/editor-api",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,14 +16,14 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "2.1.4", "@node-red/util": "2.1.6",
"@node-red/editor-client": "2.1.4", "@node-red/editor-client": "2.1.6",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"body-parser": "1.19.0", "body-parser": "1.19.1",
"clone": "2.1.2", "clone": "2.1.2",
"cors": "2.8.5", "cors": "2.8.5",
"express-session": "1.17.2", "express-session": "1.17.2",
"express": "4.17.1", "express": "4.17.2",
"memorystore": "1.6.6", "memorystore": "1.6.6",
"mime": "2.5.2", "mime": "2.5.2",
"multer": "1.4.3", "multer": "1.4.3",
@ -31,7 +31,7 @@
"oauth2orize": "1.11.1", "oauth2orize": "1.11.1",
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"passport": "0.5.0", "passport": "0.5.2",
"ws": "7.5.1" "ws": "7.5.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -59,6 +59,8 @@
"hideOtherFlows": "他のフローを非表示", "hideOtherFlows": "他のフローを非表示",
"showAllFlows": "全てのフローを表示", "showAllFlows": "全てのフローを表示",
"hideAllFlows": "全てのフローを非表示", "hideAllFlows": "全てのフローを非表示",
"hiddenFlows": "__count__ 個の非表示のフロー一覧",
"hiddenFlows_plural": "__count__ 個の非表示のフロー一覧",
"showLastHiddenFlow": "最後に非表示にしたフローを表示", "showLastHiddenFlow": "最後に非表示にしたフローを表示",
"listFlows": "フロー一覧", "listFlows": "フロー一覧",
"listSubflows": "サブフロー一覧", "listSubflows": "サブフロー一覧",
@ -669,7 +671,8 @@
"unusedConfigNodes": "未使用の設定ノード", "unusedConfigNodes": "未使用の設定ノード",
"invalidNodes": "不正なノード", "invalidNodes": "不正なノード",
"uknownNodes": "未知のノード", "uknownNodes": "未知のノード",
"unusedSubflows": "未使用のサブフロー" "unusedSubflows": "未使用のサブフロー",
"hiddenFlows": "非表示のフロー"
} }
}, },
"help": { "help": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/editor-client", "name": "@node-red/editor-client",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -71,6 +71,7 @@ RED.clipboard = (function() {
text: RED._("common.label.cancel"), text: RED._("common.label.cancel"),
click: function() { click: function() {
$( this ).dialog( "close" ); $( this ).dialog( "close" );
RED.view.focus();
} }
}, },
{ // red-ui-clipboard-dialog-download { // red-ui-clipboard-dialog-download
@ -81,6 +82,7 @@ RED.clipboard = (function() {
var data = $("#red-ui-clipboard-dialog-export-text").val(); var data = $("#red-ui-clipboard-dialog-export-text").val();
downloadData("flows.json", data); downloadData("flows.json", data);
$( this ).dialog( "close" ); $( this ).dialog( "close" );
RED.view.focus();
} }
}, },
{ // red-ui-clipboard-dialog-export { // red-ui-clipboard-dialog-export
@ -95,6 +97,7 @@ RED.clipboard = (function() {
$( this ).dialog( "close" ); $( this ).dialog( "close" );
copyText(flowData); copyText(flowData);
RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"}); RED.notify(RED._("clipboard.nodesExported"),{id:"clipboard"});
RED.view.focus();
} else { } else {
var flowToExport = $("#red-ui-clipboard-dialog-export-text").val(); var flowToExport = $("#red-ui-clipboard-dialog-export-text").val();
var selectedPath = activeLibraries[activeTab].getSelected(); var selectedPath = activeLibraries[activeTab].getSelected();
@ -110,6 +113,7 @@ RED.clipboard = (function() {
contentType: "application/json; charset=utf-8" contentType: "application/json; charset=utf-8"
}).done(function() { }).done(function() {
$(dialog).dialog( "close" ); $(dialog).dialog( "close" );
RED.view.focus();
RED.notify(RED._("library.exportedToLibrary"),"success"); RED.notify(RED._("library.exportedToLibrary"),"success");
}).fail(function(xhr,textStatus,err) { }).fail(function(xhr,textStatus,err) {
if (xhr.status === 401) { if (xhr.status === 401) {
@ -171,6 +175,7 @@ RED.clipboard = (function() {
} }
} }
$( this ).dialog( "close" ); $( this ).dialog( "close" );
RED.view.focus();
} }
}, },
{ // red-ui-clipboard-dialog-import-conflict { // red-ui-clipboard-dialog-import-conflict
@ -203,6 +208,7 @@ RED.clipboard = (function() {
// console.table(pendingImportConfig.importNodes.map(function(n) { return {id:n.id,type:n.type,result:importMap[n.id]}})) // console.table(pendingImportConfig.importNodes.map(function(n) { return {id:n.id,type:n.type,result:importMap[n.id]}}))
RED.view.importNodes(newNodes, pendingImportConfig.importOptions); RED.view.importNodes(newNodes, pendingImportConfig.importOptions);
$( this ).dialog( "close" ); $( this ).dialog( "close" );
RED.view.focus();
} }
} }
], ],
@ -940,7 +946,8 @@ RED.clipboard = (function() {
if (truncated) { if (truncated) {
msg += "_truncated"; msg += "_truncated";
} }
$("#red-ui-clipboard-hidden").val(value).focus().select(); var clipboardHidden = $('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo(document.body);
clipboardHidden.val(value).focus().select();
var result = document.execCommand("copy"); var result = document.execCommand("copy");
if (result && element) { if (result && element) {
var popover = RED.popover.create({ var popover = RED.popover.create({
@ -954,14 +961,13 @@ RED.clipboard = (function() {
},1000); },1000);
popover.open(); popover.open();
} }
$("#red-ui-clipboard-hidden").val(""); clipboardHidden.remove();
if (currentFocus) { if (currentFocus) {
$(currentFocus).focus(); $(currentFocus).focus();
} }
return result; return result;
} }
function importNodes(nodesStr,addFlow) { function importNodes(nodesStr,addFlow) {
var newNodes = nodesStr; var newNodes = nodesStr;
if (typeof nodesStr === 'string') { if (typeof nodesStr === 'string') {
@ -1236,8 +1242,6 @@ RED.clipboard = (function() {
init: function() { init: function() {
setupDialogs(); setupDialogs();
$('<textarea type="text" id="red-ui-clipboard-hidden" tabIndex="-1">').appendTo("#red-ui-editor");
RED.actions.add("core:show-export-dialog",showExportNodes); RED.actions.add("core:show-export-dialog",showExportNodes);
RED.actions.add("core:show-import-dialog",showImportNodes); RED.actions.add("core:show-import-dialog",showImportNodes);

View File

@ -578,7 +578,7 @@ RED.tabs = (function() {
function findPreviousVisibleTab(li) { function findPreviousVisibleTab(li) {
if (!li) { if (!li) {
li = ul.find("li.active").parent(); li = ul.find("li.active");
} }
var previous = li.prev(); var previous = li.prev();
while(previous.length > 0 && previous.hasClass("hide-tab")) { while(previous.length > 0 && previous.hasClass("hide-tab")) {
@ -588,9 +588,9 @@ RED.tabs = (function() {
} }
function findNextVisibleTab(li) { function findNextVisibleTab(li) {
if (!li) { if (!li) {
li = ul.find("li.active").parent(); li = ul.find("li.active");
} }
var next = ul.find("li.active").next(); var next = li.next();
while(next.length > 0 && next.hasClass("hide-tab")) { while(next.length > 0 && next.hasClass("hide-tab")) {
next = next.next(); next = next.next();
} }

View File

@ -247,7 +247,7 @@
var currentExpression = expressionEditor.getValue(); var currentExpression = expressionEditor.getValue();
var expr; var expr;
var usesContext = false; var usesContext = false;
var legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression); var legacyMode = /(^|[^a-zA-Z0-9_'".])msg([^a-zA-Z0-9_'"]|$)/.test(currentExpression);
$(".red-ui-editor-type-expression-legacy").toggle(legacyMode); $(".red-ui-editor-type-expression-legacy").toggle(legacyMode);
try { try {
expr = jsonata(currentExpression); expr = jsonata(currentExpression);

View File

@ -81,7 +81,8 @@
clearTimeout: true, clearTimeout: true,
setInterval: true, setInterval: true,
clearInterval: true clearInterval: true
} },
extraLibs: options.extraLibs
}); });
if (options.cursor) { if (options.cursor) {
expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false); expressionEditor.gotoLine(options.cursor.row+1,options.cursor.column,false);

View File

@ -55,7 +55,9 @@
} }
}); });
} }
if (!isSameObj(old_env, new_env)) { if (!old_env && new_env.length === 0) {
delete node.env;
} else if (!isSameObj(old_env, new_env)) {
editState.changes.env = node.env; editState.changes.env = node.env;
if (new_env.length === 0) { if (new_env.length === 0) {
delete node.env; delete node.env;

View File

@ -109,7 +109,7 @@ RED.utils = (function() {
window._marked.use({extensions: [descriptionList, description] } ); window._marked.use({extensions: [descriptionList, description] } );
function renderMarkdown(txt) { function renderMarkdown(txt) {
var rendered = _marked(txt); var rendered = _marked.parse(txt);
var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true}) var cleaned = DOMPurify.sanitize(rendered, {SAFE_FOR_JQUERY: true})
return cleaned; return cleaned;
} }

View File

@ -596,7 +596,7 @@ RED.view = (function() {
}, },
tooltip: function(d) { tooltip: function(d) {
if (d.validationErrors && d.validationErrors.length > 0) { if (d.validationErrors && d.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - ") return RED._("editor.errors.invalidProperties")+"\n - "+d.validationErrors.join("\n - ")
} }
}, },
show: function(n) { return !n.valid } show: function(n) { return !n.valid }

View File

@ -66,7 +66,7 @@ RED.workspaces = (function() {
var tabId = RED.nodes.id(); var tabId = RED.nodes.id();
do { do {
workspaceIndex += 1; workspaceIndex += 1;
} while ($("#red-ui-workspace-tabs a[title='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0); } while ($("#red-ui-workspace-tabs li[flowname='"+RED._('workspace.defaultName',{number:workspaceIndex})+"']").size() !== 0);
ws = { ws = {
type: "tab", type: "tab",
@ -79,12 +79,15 @@ RED.workspaces = (function() {
}; };
RED.nodes.addWorkspace(ws,targetIndex); RED.nodes.addWorkspace(ws,targetIndex);
workspace_tabs.addTab(ws,targetIndex); workspace_tabs.addTab(ws,targetIndex);
workspace_tabs.activateTab(tabId); workspace_tabs.activateTab(tabId);
if (!skipHistoryEntry) { if (!skipHistoryEntry) {
RED.history.push({t:'add',workspaces:[ws],dirty:RED.nodes.dirty()}); RED.history.push({t:'add',workspaces:[ws],dirty:RED.nodes.dirty()});
RED.nodes.dirty(true); RED.nodes.dirty(true);
} }
} }
$("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
RED.view.focus(); RED.view.focus();
return ws; return ws;
} }
@ -208,10 +211,20 @@ RED.workspaces = (function() {
}, },
onhide: function(tab) { onhide: function(tab) {
hideStack.push(tab.id); hideStack.push(tab.id);
var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
hiddenTabs[tab.id] = true;
RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
RED.events.emit("workspace:hide",{workspace: tab.id}) RED.events.emit("workspace:hide",{workspace: tab.id})
}, },
onshow: function(tab) { onshow: function(tab) {
removeFromHideStack(tab.id); removeFromHideStack(tab.id);
var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
delete hiddenTabs[tab.id];
RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
RED.events.emit("workspace:show",{workspace: tab.id}) RED.events.emit("workspace:show",{workspace: tab.id})
}, },
minimumActiveTabWidth: 150, minimumActiveTabWidth: 150,
@ -542,9 +555,6 @@ RED.workspaces = (function() {
} }
if (workspace_tabs.contains(id)) { if (workspace_tabs.contains(id)) {
workspace_tabs.hideTab(id); workspace_tabs.hideTab(id);
var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
hiddenTabs[id] = true;
RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
} }
}, },
isHidden: function(id) { isHidden: function(id) {
@ -572,14 +582,11 @@ RED.workspaces = (function() {
} }
workspace_tabs.activateTab(id); workspace_tabs.activateTab(id);
} }
var hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
delete hiddenTabs[id];
RED.settings.setLocal("hiddenTabs",JSON.stringify(hiddenTabs));
}, },
refresh: function() { refresh: function() {
RED.nodes.eachWorkspace(function(ws) { RED.nodes.eachWorkspace(function(ws) {
workspace_tabs.renameTab(ws.id,ws.label); workspace_tabs.renameTab(ws.id,ws.label);
$("#red-ui-tab-"+(ws.id.replace(".","-"))).attr("flowname",ws.label)
}) })
RED.nodes.eachSubflow(function(sf) { RED.nodes.eachSubflow(function(sf) {
if (workspace_tabs.contains(sf.id)) { if (workspace_tabs.contains(sf.id)) {

View File

@ -360,6 +360,7 @@ button.red-ui-button-small
position: absolute; position: absolute;
top: -3000px; top: -3000px;
} }
.form-row .red-ui-editor-node-label-form-row { .form-row .red-ui-editor-node-label-form-row {
margin: 5px 0 0 50px; margin: 5px 0 0 50px;
label { label {

View File

@ -73,8 +73,8 @@ export default {
}, },
element: "#red-ui-workspace-tabs > li.active", element: "#red-ui-workspace-tabs > li.active",
description: { description: {
"en-US": '<p>Tabs can now be hidden by clicking their <i class="fa fa-times"></i> icon.</p><p>The Info Sidebar will still list all of your tabs, and tell you which ones are currently hidden.', "en-US": '<p>Tabs can now be hidden by clicking their <i class="fa fa-eye-slash"></i> icon.</p><p>The Info Sidebar will still list all of your tabs, and tell you which ones are currently hidden.',
"ja": '<p><i class="fa fa-times"></i> アイコンをクリックすることで、タブを非表示にできます。</p><p>情報サイドバーには、全てのタブが一覧表示されており、現在非表示になっているタブを確認できます。' "ja": '<p><i class="fa fa-eye-slash"></i> アイコンをクリックすることで、タブを非表示にできます。</p><p>情報サイドバーには、全てのタブが一覧表示されており、現在非表示になっているタブを確認できます。'
}, },
interactive: false, interactive: false,
prepare() { prepare() {

View File

@ -0,0 +1,711 @@
<!--
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.
-->
<script type="text/html" data-template-name="inject">
<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">
</div>
<div class="form-row node-input-property-container-row">
<ol id="node-input-property-container"></ol>
</div>
<div class="form-row" id="node-once">
<label for="node-input-once">&nbsp;</label>
<input type="checkbox" id="node-input-once" style="display:inline-block; width:15px; vertical-align:baseline;">
<span data-i18n="inject.onstart"></span>&nbsp;
<input type="text" id="node-input-onceDelay" placeholder="0.1" style="width:45px; height:28px;">&nbsp;
<span data-i18n="inject.onceDelay"></span>
</div>
<div class="form-row">
<label for=""><i class="fa fa-repeat"></i> <span data-i18n="inject.label.repeat"></span></label>
<select id="inject-time-type-select">
<option value="none" data-i18n="inject.none"></option>
<option value="interval" data-i18n="inject.interval"></option>
<option value="interval-time" data-i18n="inject.interval-time"></option>
<option value="time" data-i18n="inject.time"></option>
</select>
<input type="hidden" id="node-input-repeat">
<input type="hidden" id="node-input-crontab">
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-interval">
<span data-i18n="inject.every"></span>
<input id="inject-time-interval-count" class="inject-time-count" value="1"></input>
<select style="width:100px" id="inject-time-interval-units">
<option value="s" data-i18n="inject.seconds"></option>
<option value="m" data-i18n="inject.minutes"></option>
<option value="h" data-i18n="inject.hours"></option>
</select><br/>
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-interval-time">
<span data-i18n="inject.every"></span> <select style="width:90px; margin-left:20px;" id="inject-time-interval-time-units" class="inject-time-int-count" value="1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="10">10</option>
<option value="12">12</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="0">60</option>
</select> <span data-i18n="inject.minutes"></span><br/>
<span data-i18n="inject.between"></span> <select id="inject-time-interval-time-start" class="inject-time-times"></select>
<span data-i18n="inject.and"></span> <select id="inject-time-interval-time-end" class="inject-time-times"></select><br/>
<div id="inject-time-interval-time-days" class="inject-time-days" style="margin-top:5px">
<div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="inject.on">on</div>
<div style="display:inline-block;">
<div>
<label><input type='checkbox' checked value='1'/> <span data-i18n="inject.days.0"></span></label>
<label><input type='checkbox' checked value='2'/> <span data-i18n="inject.days.1"></span></label>
<label><input type='checkbox' checked value='3'/> <span data-i18n="inject.days.2"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='4'/> <span data-i18n="inject.days.3"></span></label>
<label><input type='checkbox' checked value='5'/> <span data-i18n="inject.days.4"></span></label>
<label><input type='checkbox' checked value='6'/> <span data-i18n="inject.days.5"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='0'/> <span data-i18n="inject.days.6"></span></label>
</div>
</div>
</div>
</div>
<div class="form-row inject-time-row hidden" id="inject-time-row-time">
<span data-i18n="inject.at"></span> <input type="text" id="inject-time-time" value="12:00"></input><br/>
<div id="inject-time-time-days" class="inject-time-days">
<div style="display:inline-block; vertical-align:top; margin-right:5px;" data-i18n="inject.on"></div>
<div style="display:inline-block;">
<div>
<label><input type='checkbox' checked value='1'/> <span data-i18n="inject.days.0"></span></label>
<label><input type='checkbox' checked value='2'/> <span data-i18n="inject.days.1"></span></label>
<label><input type='checkbox' checked value='3'/> <span data-i18n="inject.days.2"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='4'/> <span data-i18n="inject.days.3"></span></label>
<label><input type='checkbox' checked value='5'/> <span data-i18n="inject.days.4"></span></label>
<label><input type='checkbox' checked value='6'/> <span data-i18n="inject.days.5"></span></label>
</div>
<div>
<label><input type='checkbox' checked value='0'/> <span data-i18n="inject.days.6"></span></label>
</div>
</div>
</div>
</div>
</script>
<style>
.inject-time-row {
padding-left: 110px;
}
.inject-time-row select {
margin: 3px 0;
}
.inject-time-days label {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: baseline;
width: 100px;
}
.inject-time-days input {
width: auto !important;
vertical-align: baseline !important;
}
.inject-time-times {
width: 90px !important;
}
#inject-time-time {
width: 75px;
margin-left: 8px;
margin-bottom: 8px;
}
.inject-time-count {
padding-left: 3px !important;
width: 80px !important;
}
</style>
<script type="text/javascript">
(function() {
function resizeDialog(size) {
size = size || { height: $(".red-ui-tray-content form").height() }
var rows = $("#dialog-form>div:not(.node-input-property-container-row):visible");
var height = size.height;
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-property-container-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
height += 16;
$("#node-input-property-container").editableList('height',height);
}
/** Retrieve editableList items (refactored for re-use in the form inject button)*/
function getProps(el, legacy) {
var result = {
props: []
}
el.each(function(i) {
var prop = $(this);
var p = {
p:prop.find(".node-input-prop-property-name").typedInput('value')
};
if (p.p) {
p.v = prop.find(".node-input-prop-property-value").typedInput('value');
p.vt = prop.find(".node-input-prop-property-value").typedInput('type');
if(legacy) {
if (p.p === "payload") { // save payload to old "legacy" property
result.payloadType = p.vt;
result.payload = p.v;
delete p.v;
delete p.vt;
} else if (p.p === "topic" && p.vt === "str") {
result.topic = p.v;
delete p.v;
}
}
result.props.push(p);
}
});
return result;
}
/** Perform inject, optionally sending a custom msg (refactored for re-use in the form inject button)*/
function doInject(node, customMsg) {
var label = node._def.label.call(node,customMsg?customMsg.__user_inject_props__:undefined);
if (label.length > 30) {
label = label.substring(0, 50) + "...";
}
label = label.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify(customMsg||{}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("inject.success", { label: label }), { type: "success", id: "inject", timeout: 2000 });
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.not-deployed") }), "error");
} else if (jqXHR.status == 500) {
RED.notify(node._("common.notification.error", { message: node._("inject.errors.failed") }), "error");
} else if (jqXHR.status == 0) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.no-response") }), "error");
} else {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.unexpected", { status: jqXHR.status, message: textStatus }) }), "error");
}
}
});
}
RED.nodes.registerType('inject',{  
category: 'common',
color:"#a6bbcf",
defaults: {
name: {value:""},
props:{value:[{p:"payload"},{p:"topic",vt:"str"}], validate:function(v) {
if (!v || v.length === 0) { return true }
for (var i=0;i<v.length;i++) {
if (/msg|flow|global/.test(v[i].vt)) {
if (!RED.utils.validatePropertyExpression(v[i].v)) {
return false;
}
} else if (v[i].vt === "jsonata") {
try{jsonata(v[i].v);}catch(e){return false;}
} else if (v[i].vt === "json") {
try{JSON.parse(v[i].v);}catch(e){return false;}
}
}
return true;
}
},
repeat: {value:"", validate:function(v) { return ((v === "") || (RED.validators.number(v) && (v >= 0) && (v <= 2147483))) }},
crontab: {value:""},
once: {value:false},
onceDelay: {value:0.1},
topic: {value:""},
payload: {value:"", validate: RED.validators.typedInput("payloadType")},
payloadType: {value:"date"},
},
icon: "inject.svg",
inputs:0,
outputs:1,
outputLabels: function(index) {
var lab = '';
// if only payload and topic - display payload type
// if only one property - show it's type
// if more than one property (other than payload and topic) - show "x properties" where x is the number of properties.
// this.props will not be an array for legacy inject nodes until they are re-deployed
//
var props = this.props;
if (!Array.isArray(props)) {
props = [
{ p:"payload", v: this.payload, vt: this.payloadType },
{ p:"topic", v: this.topic, vt: "str" }
]
}
if (props) {
for (var i=0,l=props.length; i<l; i++) {
if (i > 0) lab += "\n";
if (i === 5) {
lab += "... +"+(props.length-5);
break;
}
lab += props[i].p+": ";
var propType = props[i].p === "payload"? this.payloadType : props[i].vt;
if (propType === "json") {
try {
var parsedProp = JSON.parse(props[i].p === "payload"? this.payload : props[i].v);
propType = typeof parsedProp;
if (propType === "object" && Array.isArray(parsedProp)) {
propType = "Array";
}
} catch(e) {
propType = "invalid";
}
}
lab += this._("inject.label."+propType);
}
}
return lab;
},
label: function(customProps) {
var suffix = "";
// if fire once then add small indication
if (this.once) {
suffix = " ¹";
}
// but replace with repeat one if set to repeat
if ((this.repeat && this.repeat != 0) || this.crontab) {
suffix = " ↻";
}
if (this.name) {
return this.name+suffix;
}
var payload = "";
var payloadType = "str";
var topic = "";
if (customProps) {
for (var i=0;i<customProps.length;i++) {
if (customProps[i].p === "payload") {
payload = customProps[i].v;
payloadType = customProps[i].vt;
} else if (customProps[i].p === "topic") {
topic = customProps[i].v;
}
}
} else {
payload = this.payload || "";
payloadType = this.payloadType || "str";
topic = this.topic || "";
}
if (payloadType === "string" ||
payloadType === "str" ||
payloadType === "num" ||
payloadType === "bool" ||
payloadType === "json") {
if ((topic !== "") && ((topic.length + payload.length) <= 32)) {
return topic + ":" + payload+suffix;
} else if (payload.length > 0 && payload.length < 24) {
return payload+suffix;
} else {
return this._("inject.inject")+suffix;
}
} else if (payloadType === 'date' || payloadType === 'bin' || payloadType === 'env') {
if ((topic !== "") && (topic.length <= 16)) {
return topic + ":" + this._('inject.label.'+payloadType)+suffix;
} else {
return this._('inject.label.'+payloadType)+suffix;
}
} else if (payloadType === 'flow' || payloadType === 'global') {
var key = RED.utils.parseContextKey(payload);
return payloadType+"."+key.key+suffix;
} else {
return this._("inject.inject")+suffix;
}
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
var payloadType = node.payloadType;
if (node.payloadType == null) {
if (node.payload == "") {
payloadType = "date";
} else {
payloadType = "str";
}
} else if (node.payloadType === 'string' || node.payloadType === 'none') {
payloadType = "str";
}
$("#inject-time-type-select").on("change", function() {
$("#node-input-crontab").val('');
var id = $("#inject-time-type-select").val();
$(".inject-time-row").hide();
$("#inject-time-row-"+id).show();
if ((id == "none") || (id == "interval") || (id == "interval-time")) {
$("#node-once").show();
}
else {
$("#node-once").hide();
$("#node-input-once").prop('checked', false);
}
// Scroll down
var scrollDiv = $("#dialog-form").parent();
scrollDiv.scrollTop(scrollDiv.prop('scrollHeight'));
resizeDialog();
});
$("#node-input-once").on("change", function() {
$("#node-input-onceDelay").attr('disabled', !$("#node-input-once").prop('checked'));
})
$(".inject-time-times").each(function() {
for (var i=0; i<24; i++) {
var l = (i<10?"0":"")+i+":00";
$(this).append($("<option></option>").val(i).text(l));
}
});
$("<option></option>").val(24).text("00:00").appendTo("#inject-time-interval-time-end");
$("#inject-time-interval-time-start").on("change", function() {
var start = Number($("#inject-time-interval-time-start").val());
var end = Number($("#inject-time-interval-time-end").val());
$("#inject-time-interval-time-end option").remove();
for (var i=start+1; i<25; i++) {
var l = (i<10?"0":"")+i+":00";
if (i==24) {
l = "00:00";
}
var opt = $("<option></option>").val(i).text(l).appendTo("#inject-time-interval-time-end");
if (i === end) {
opt.attr("selected","selected");
}
}
});
$(".inject-time-count").spinner({
//max:60,
min:1
});
var repeattype = "none";
if (node.repeat != "" && node.repeat != 0) {
repeattype = "interval";
var r = "s";
var c = node.repeat;
if (node.repeat % 60 === 0) { r = "m"; c = c/60; }
if (node.repeat % 1440 === 0) { r = "h"; c = c/60; }
$("#inject-time-interval-count").val(c);
$("#inject-time-interval-units").val(r);
$("#inject-time-interval-days").prop("disabled","disabled");
} else if (node.crontab) {
var cronparts = node.crontab.split(" ");
var days = cronparts[4];
if (!isNaN(cronparts[0]) && !isNaN(cronparts[1])) {
repeattype = "time";
// Fixed time
var time = cronparts[1]+":"+cronparts[0];
$("#inject-time-time").val(time);
$("#inject-time-type-select").val("s");
if (days == "*") {
$("#inject-time-time-days input[type=checkbox]").prop("checked",true);
} else {
$("#inject-time-time-days input[type=checkbox]").removeAttr("checked");
days.split(",").forEach(function(v) {
$("#inject-time-time-days [value=" + v + "]").prop("checked", true);
});
}
} else {
repeattype = "interval-time";
// interval - time period
var minutes = cronparts[0].slice(2);
if (minutes === "") { minutes = "0"; }
$("#inject-time-interval-time-units").val(minutes);
if (days == "*") {
$("#inject-time-interval-time-days input[type=checkbox]").prop("checked",true);
} else {
$("#inject-time-interval-time-days input[type=checkbox]").removeAttr("checked");
days.split(",").forEach(function(v) {
$("#inject-time-interval-time-days [value=" + v + "]").prop("checked", true);
});
}
var time = cronparts[1];
var timeparts = time.split(",");
var start;
var end;
if (timeparts.length == 1) {
// 0 or 0-10
var hours = timeparts[0].split("-");
if (hours.length == 1) {
if (hours[0] === "") {
start = "0";
end = "0";
}
else {
start = hours[0];
end = Number(hours[0])+1;
}
} else {
start = hours[0];
end = Number(hours[1])+1;
}
} else {
// 23,0 or 17-23,0-10 or 23,0-2 or 17-23,0
var startparts = timeparts[0].split("-");
start = startparts[0];
var endparts = timeparts[1].split("-");
if (endparts.length == 1) {
end = Number(endparts[0])+1;
} else {
end = Number(endparts[1])+1;
}
}
$("#inject-time-interval-time-end").val(end);
$("#inject-time-interval-time-start").val(start);
}
} else {
$("#inject-time-type-select").val("none");
}
$(".inject-time-row").hide();
$("#inject-time-type-select").val(repeattype);
$("#inject-time-row-"+repeattype).show();
/* */
var eList = $('#node-input-property-container').css('min-height','120px').css('min-width','450px');
eList.editableList({
buttons: [
{
id: "node-inject-test-inject-button",
label: node._("inject.injectNow"),
click: function(e) {
var items = eList.editableList('items');
var props = getProps(items);
var m = {__user_inject_props__: props.props};
doInject(node, m);
}
}
],
addItem: function(container,i,opt) {
var prop = opt;
if (!prop.hasOwnProperty('p')) {
prop = {p:"",v:"",vt:"str"};
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
var propertyName = $('<input/>',{class:"node-input-prop-property-name",type:"text"})
.css("width","30%")
.appendTo(row)
.typedInput({types:['msg']});
$('<div/>',{style: 'display:inline-block; padding:0px 6px;'})
.text('=')
.appendTo(row);
var propertyValue = $('<input/>',{class:"node-input-prop-property-value",type:"text"})
.css("width","calc(70% - 30px)")
.appendTo(row)
.typedInput({default:prop.vt || 'str',types:['flow','global','str','num','bool','json','bin','date','jsonata','env','msg']});
propertyName.typedInput('value',prop.p);
propertyValue.typedInput('value',prop.v);
},
removable: true,
sortable: true
});
$('#node-inject-test-inject-button').css("float", "right").css("margin-right", "unset");
if (RED.nodes.subflow(node.z)) {
$('#node-inject-test-inject-button').attr("disabled",true);
}
if (!node.props) {
var payload = {
p:'payload',
v: node.payload ? node.payload : '',
vt:payloadType ? payloadType : 'date'
};
var topic = {
p:'topic',
v: node.topic ? node.topic : '',
vt:'str'
}
node.props = [payload,topic];
}
for (var i=0; i<node.props.length; i++) {
var prop = node.props[i];
var newProp = { p: prop.p, v: prop.v, vt: prop.vt };
if (newProp.v === undefined) {
if (prop.p === 'payload') {
newProp.v = node.payload ? node.payload : '';
newProp.vt = payloadType ? payloadType : 'date';
} else if (prop.p === 'topic' && prop.vt === "str") {
newProp.v = node.topic ? node.topic : '';
}
}
if (newProp.vt === "string") {
// Fix bug in pre 2.1 where an old Inject node might have
// a migrated rule with type 'string' not 'str'
newProp.vt = "str";
}
eList.editableList('addItem',newProp);
}
$("#inject-time-type-select").trigger("change");
$("#inject-time-interval-time-start").trigger("change");
},
oneditsave: function() {
var repeat = "";
var crontab = "";
var type = $("#inject-time-type-select").val();
if (type == "none") {
// nothing
} else if (type == "interval") {
var count = $("#inject-time-interval-count").val();
var units = $("#inject-time-interval-units").val();
if (units == "s") {
repeat = count;
} else {
if (units == "m") {
//crontab = "*/"+count+" * * * "+days;
repeat = count * 60;
} else if (units == "h") {
//crontab = "0 */"+count+" * * "+days;
repeat = count * 60 * 60;
}
}
} else if (type == "interval-time") {
repeat = "";
var count = $("#inject-time-interval-time-units").val();
var startTime = Number($("#inject-time-interval-time-start").val());
var endTime = Number($("#inject-time-interval-time-end").val());
var days = $('#inject-time-interval-time-days input[type=checkbox]:checked').map(function(_, el) {
return $(el).val()
}).get();
if (days.length == 0) {
crontab = "";
} else {
if (days.length == 7) {
days="*";
} else {
days = days.join(",");
}
var timerange = "";
if (endTime == 0) {
timerange = startTime+"-23";
} else if (startTime+1 < endTime) {
timerange = startTime+"-"+(endTime-1);
} else if (startTime+1 == endTime) {
timerange = startTime;
} else {
var startpart = "";
var endpart = "";
if (startTime == 23) {
startpart = "23";
} else {
startpart = startTime+"-23";
}
if (endTime == 1) {
endpart = "0";
} else {
endpart = "0-"+(endTime-1);
}
timerange = startpart+","+endpart;
}
if (count === "0") {
crontab = count+" "+timerange+" * * "+days;
} else {
crontab = "*/"+count+" "+timerange+" * * "+days;
}
}
} else if (type == "time") {
var time = $("#inject-time-time").val();
var days = $('#inject-time-time-days input[type=checkbox]:checked').map(function(_, el) {
return $(el).val()
}).get();
if (days.length == 0) {
crontab = "";
} else {
if (days.length == 7) {
days="*";
} else {
days = days.join(",");
}
var parts = time.split(":");
if (parts.length === 2) {
repeat = "";
parts[1] = ("00" + (parseInt(parts[1]) % 60)).substr(-2);
parts[0] = ("00" + (parseInt(parts[0]) % 24)).substr(-2);
crontab = parts[1]+" "+parts[0]+" * * "+days;
}
else { crontab = ""; }
}
}
$("#node-input-repeat").val(repeat);
$("#node-input-crontab").val(crontab);
/* Gather the properties */
var items = $("#node-input-property-container").editableList('items');
delete this.payloadType;
delete this.payload;
this.topic = "";
var result = getProps(items, true);
this.props = result.props;
if(result.hasOwnProperty('payloadType')) { this.payloadType = result.payloadType; };
if(result.hasOwnProperty('payload')) { this.payload = result.payload; };
if(result.hasOwnProperty('topic')) { this.topic = result.topic; };
},
button: {
enabled: function() {
return !this.changed
},
onclick: function () {
if (this.changed) {
return RED.notify(RED._("notification.warning", { message: RED._("notification.warnings.undeployedChanges") }), "warning");
}
doInject(this);
}
},
oneditresize: resizeDialog
});
})();
</script>

View File

@ -0,0 +1,611 @@
<script type="text/html" data-template-name="function">
<style>
.func-tabs-row {
margin-bottom: 0;
}
#node-input-libs-container-row .red-ui-editableList-container {
padding: 0px;
}
#node-input-libs-container-row .red-ui-editableList-container li {
padding:0px;
}
#node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px;
}
#node-input-libs-container-row .red-ui-editableList-header {
display: flex;
background: var(--red-ui-tertiary-background);
padding-right: 75px;
}
#node-input-libs-container-row .red-ui-editableList-header > div {
flex-grow: 1;
}
.node-libs-entry {
display: flex;
}
.node-libs-entry .red-ui-typedInput-container {
border-radius: 0;
border: none;
}
.node-libs-entry .red-ui-typedInput-type-select {
border-radius: 0 !important;
height: 34px;
}
.node-libs-entry > span > input[type=text] {
border-radius: 0;
border-top-color: var(--red-ui-form-background);
border-bottom-color: var(--red-ui-form-background);
border-right-color: var(--red-ui-form-background);
}
.node-libs-entry > span > input[type=text].input-error {
}
.node-libs-entry > span {
flex-grow: 1;
width: 50%;
position: relative;
}
.node-libs-entry span .node-input-libs-var, .node-libs-entry span .red-ui-typedInput-container {
width: 100%;
}
.node-libs-entry > span > span > i {
display: none;
}
.node-libs-entry > span > span.input-error > i {
display: inline;
}
</style>
<input type="hidden" id="node-input-func">
<input type="hidden" id="node-input-noerr">
<input type="hidden" id="node-input-finalize">
<input type="hidden" id="node-input-initialize">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
</div>
<div class="form-row func-tabs-row">
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
</div>
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
<div id="func-tab-config" style="display:none">
<div class="form-row">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px;" value="1">
</div>
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
<label><i class="fa fa-cubes"></i> <span data-i18n="function.label.modules"></span></label>
</div>
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
<ol id="node-input-libs-container"></ol>
</div>
</div>
<div id="func-tab-init" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
<div id="func-tab-finalize" style="display:none">
<div class="form-row node-text-editor-row" style="position:relative">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
<div style="position: absolute; right:0; bottom: calc(100% - 20px); z-Index: 10;"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function() {
var invalidModuleVNames = [
'console',
'util',
'Buffer',
'Date',
'RED',
'node',
'__node__',
'context',
'flow',
'global',
'env',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'promisify'
]
var knownFunctionNodes = {};
RED.events.on("nodes:add", function(n) {
if (n.type === "function") {
knownFunctionNodes[n.id] = n;
}
})
RED.events.on("nodes:remove", function(n) {
if (n.type === "function") {
delete knownFunctionNodes[n.id];
}
})
var missingModules = [];
var missingModuleReasons = {};
RED.events.on("runtime-state", function(event) {
if (event.error === "missing-modules") {
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
} else if (!event.text) {
missingModuleReasons = {};
missingModules = [];
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
RED.editor.validateNode(knownFunctionNodes[id])
}
}
}
RED.view.redraw();
});
var installAllowList = ['*'];
var installDenyList = [];
var modulesEnabled = true;
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
modulesEnabled = false;
}
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
if (settingsAllowList || settingsDenyList) {
installAllowList = settingsAllowList;
installDenyList = settingsDenyList
}
installAllowList = RED.utils.parseModuleList(installAllowList);
installDenyList = RED.utils.parseModuleList(installDenyList);
// object that maps from library name to its descriptor
var allLibs = [];
function moduleName(module) {
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
function getAllUsedModules() {
var moduleSet = new Set();
for (var id in knownFunctionNodes) {
if (knownFunctionNodes.hasOwnProperty(id)) {
if (knownFunctionNodes[id].libs) {
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
moduleSet.add(knownFunctionNodes[id].libs[i].module);
}
}
}
}
}
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$(".node-input-libs-row").show();
var usedModules = getAllUsedModules();
var typedModules = usedModules.map(function(l) {
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
})
typedModules.push({
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
})
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
header: $('<div><div data-i18n="node-red:function.require.moduleName"></div><div data-i18n="node-red:function.require.importAs"></div></div>'),
addItem: function(container,i,opt) {
var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fmoduleSpan = $("<span>").appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text"
}).css({
}).appendTo(fmoduleSpan).typedInput({
types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
});
if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module);
}
var moduleWarning = $('<span style="position: absolute;right:2px;top:7px; display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fmoduleSpan);
RED.popover.tooltip(moduleWarning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
var errors = [];
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return RED._("node-red:function.error.moduleNotAllowed",{module:val});
} else {
return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]});
}
})
var fvarSpan = $("<span>").appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
}).appendTo(fvarSpan).val(opt.var);
var vnameWarning = $('<span style="position: absolute; right:2px;top:7px;display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fvarSpan);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
fvar.addClass("input-error");
vnameWarning.addClass("input-error");
} else {
fvar.removeClass("input-error");
vnameWarning.removeClass("input-error");
}
});
fmodule.on("change keyup paste", function (e) {
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/].?/g, function(v) { return v[1]?v[1].toUpperCase():"" });
fvar.val(varName);
fvar.trigger("change");
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
moduleWarning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
moduleWarning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true
});
var libs = node.libs || [];
for (var i=0,l=libs.length;i<l; i++) {
libList.editableList('addItem',libs[i])
}
}
function getLibsList() {
var _libs = [];
if (RED.settings.functionExternalModules !== false) {
var libs = $("#node-input-libs-container").editableList("items");
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").typedInput("type");
if (n === "_custom_") {
n = item.find(".node-input-libs-val").val();
}
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
_libs.push({
var: v,
module: n
});
});
}
return _libs;
}
RED.nodes.registerType('function',{
color:"#fdd0a2",
category: 'function',
defaults: {
name: {value:""},
func: {value:"\nreturn msg;"},
outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {value: [], validate: function(v) {
if (!v) { return true; }
for (var i=0,l=v.length;i<l;i++) {
var m = v[i];
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
return false
}
if (m.var === "" || / /.test(m.var)) {
return false;
}
if (missingModules.indexOf(m.module) > -1) {
return false;
}
if (invalidModuleVNames.indexOf(m.var) !== -1){
return false;
}
}
return true;
}}
},
inputs:1,
outputs:1,
icon: "function.svg",
label: function() {
return this.name||this._("function.function");
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var that = this;
var tabs = RED.tabs.create({
id: "func-tabs",
onchange: function(tab) {
$("#func-tabs-content").children().hide();
$("#" + tab.id).show();
let editor = $("#" + tab.id).find('.monaco-editor').first();
if(editor.length) {
if(that.editor.nodered && that.editor.type == "monaco") {
that.editor.nodered.refreshModuleLibs(getLibsList());
}
RED.tray.resize();
}
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
label: that._("function.label.setup")
});
tabs.addTab({
id: "func-tab-init",
label: that._("function.label.initialize")
});
tabs.addTab({
id: "func-tab-body",
label: that._("function.label.function")
});
tabs.addTab({
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.activateTab("func-tab-body");
$( "#node-input-outputs" ).spinner({
min:0,
change: function(event, ui) {
var value = this.value;
if (!value.match(/^\d+$/)) { value = 1; }
else if (value < this.min) { value = this.min; }
if (value !== this.value) { $(this).spinner("value", value); }
}
});
var buildEditor = function(id, value, defaultValue, extraLibs) {
var editor = RED.editor.createEditor({
id: id,
mode: 'ace/mode/nrjavascript',
value: value || defaultValue || "",
globals: {
msg:true,
context:true,
RED: true,
util: true,
flow: true,
global: true,
console: true,
Buffer: true,
setTimeout: true,
clearTimeout: true,
setInterval: true,
clearInterval: true
},
extraLibs: extraLibs
});
if (defaultValue && value === "") {
editor.moveCursorTo(defaultValue.split("\n").length - 1, 0);
}
return editor;
}
this.initEditor = buildEditor('node-input-init-editor',$("#node-input-initialize").val(),RED._("node-red:function.text.initialize"))
this.editor = buildEditor('node-input-func-editor',$("#node-input-func").val(), undefined, that.libs || [])
this.finalizeEditor = buildEditor('node-input-finalize-editor',$("#node-input-finalize").val(),RED._("node-red:function.text.finalize"))
RED.library.create({
url:"functions", // where to get the data from
type:"function", // the type of object the library is for
editor:this.editor, // the field name the main text body goes to
mode:"ace/mode/nrjavascript",
fields:[
'name', 'outputs',
{
name: 'initialize',
get: function() {
return that.initEditor.getValue();
},
set: function(v) {
that.initEditor.setValue(v||RED._("node-red:function.text.initialize"), -1);
}
},
{
name: 'finalize',
get: function() {
return that.finalizeEditor.getValue();
},
set: function(v) {
that.finalizeEditor.setValue(v||RED._("node-red:function.text.finalize"), -1);
}
},
{
name: 'info',
get: function() {
return that.infoEditor.getValue();
},
set: function(v) {
that.infoEditor.setValue(v||"", -1);
}
}
],
ext:"js"
});
this.editor.focus();
var expandButtonClickHandler = function(editor) {
return function(e) {
e.preventDefault();
var value = editor.getValue();
var extraLibs = that.libs || [];
RED.editor.editJavaScript({
value: value,
width: "Infinity",
cursor: editor.getCursorPosition(),
mode: "ace/mode/nrjavascript",
complete: function(v,cursor) {
editor.setValue(v, -1);
editor.gotoLine(cursor.row+1,cursor.column,false);
setTimeout(function() {
editor.focus();
},300);
},
extraLibs: extraLibs
})
}
}
$("#node-init-expand-js").on("click", expandButtonClickHandler(this.initEditor));
$("#node-function-expand-js").on("click", expandButtonClickHandler(this.editor));
$("#node-finalize-expand-js").on("click", expandButtonClickHandler(this.finalizeEditor));
RED.popover.tooltip($("#node-init-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
},
oneditsave: function() {
var node = this;
var noerr = 0;
$("#node-input-noerr").val(0);
var disposeEditor = function(editorName,targetName,defaultValue) {
var editor = node[editorName];
var annot = editor.getSession().getAnnotations();
for (var k=0; k < annot.length; k++) {
if (annot[k].type === "error") {
noerr += annot.length;
break;
}
}
var val = editor.getValue();
if (defaultValue) {
if (val.trim() == defaultValue.trim()) {
val = "";
}
}
editor.destroy();
delete node[editorName];
$("#"+targetName).val(val);
}
disposeEditor("editor","node-input-func");
disposeEditor("initEditor","node-input-initialize", RED._("node-red:function.text.initialize"));
disposeEditor("finalizeEditor","node-input-finalize", RED._("node-red:function.text.finalize"));
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
node.libs = getLibsList();
},
oneditcancel: function() {
var node = this;
node.editor.destroy();
delete node.editor;
node.initEditor.destroy();
delete node.initEditor;
node.finalizeEditor.destroy();
delete node.finalizeEditor;
},
oneditresize: function(size) {
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
var height = $("#dialog-form").height();
for (var i=0; i<rows.length; i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-text-editor-row");
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
$("#dialog-form .node-text-editor").css("height",height+"px");
var height = size.height;
$("#node-input-init-editor").css("height", (height - 83)+"px");
$("#node-input-func-editor").css("height", (height - 83)+"px");
$("#node-input-finalize-editor").css("height", (height - 83)+"px");
this.initEditor.resize();
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height - 192)+"px");
}
});
})();
</script>

View File

@ -0,0 +1,506 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
var util = require("util");
var vm = require("vm");
var acorn = require("acorn");
var acornWalk = require("acorn-walk");
function sendResults(node,send,_msgid,msgs,cloneFirstMessage) {
if (msgs == null) {
return;
} else if (!util.isArray(msgs)) {
msgs = [msgs];
}
var msgCount = 0;
for (var m=0; m<msgs.length; m++) {
if (msgs[m]) {
if (!util.isArray(msgs[m])) {
msgs[m] = [msgs[m]];
}
for (var n=0; n < msgs[m].length; n++) {
var msg = msgs[m][n];
if (msg !== null && msg !== undefined) {
if (typeof msg === 'object' && !Buffer.isBuffer(msg) && !util.isArray(msg)) {
if (msgCount === 0 && cloneFirstMessage !== false) {
msgs[m][n] = RED.util.cloneMessage(msgs[m][n]);
msg = msgs[m][n];
}
msg._msgid = _msgid;
msgCount++;
} else {
var type = typeof msg;
if (type === 'object') {
type = Buffer.isBuffer(msg)?'Buffer':(util.isArray(msg)?'Array':'Date');
}
node.error(RED._("function.error.non-message-returned",{ type: type }));
}
}
}
}
}
if (msgCount>0) {
send(msgs);
}
}
function createVMOpt(node, kind) {
var opt = {
filename: 'Function node'+kind+':'+node.id+(node.name?' ['+node.name+']':''), // filename for stack traces
displayErrors: true
// Using the following options causes node 4/6 to not include the line number
// in the stack output. So don't use them.
// lineOffset: -11, // line number offset to be used for stack traces
// columnOffset: 0, // column number offset to be used for stack traces
};
return opt;
}
function updateErrorInfo(err) {
if (err.stack) {
var stack = err.stack.toString();
var m = /^([^:]+):([^:]+):(\d+).*/.exec(stack);
if (m) {
var line = parseInt(m[3]) -1;
var kind = "body:";
if (/setup/.exec(m[1])) {
kind = "setup:";
}
if (/cleanup/.exec(m[1])) {
kind = "cleanup:";
}
err.message += " ("+kind+"line "+line+")";
}
}
}
function FunctionNode(n) {
RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
node.func = n.func;
node.outputs = n.outputs;
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = n.libs || [];
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
throw new Error(RED._("function.error.externalModuleNotAllowed"));
}
var functionText = "var results = null;"+
"results = (async function(msg,__send__,__done__){ "+
"var __msgid__ = msg._msgid;"+
"var node = {"+
"id:__node__.id,"+
"name:__node__.name,"+
"outputCount:__node__.outputCount,"+
"log:__node__.log,"+
"error:__node__.error,"+
"warn:__node__.warn,"+
"debug:__node__.debug,"+
"trace:__node__.trace,"+
"on:__node__.on,"+
"status:__node__.status,"+
"send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+
"done:__done__"+
"};\n"+
node.func+"\n"+
"})(msg,__send__,__done__);";
var handleNodeDoneCall = true;
// Check to see if the Function appears to call `node.done()`. If so,
// we will assume it is well written and does actually call node.done().
// Otherwise, we will call node.done() after the function returns regardless.
if (/node\.done\s*\(\s*\)/.test(functionText)) {
// We have spotted the code contains `node.done`. It could be in a comment
// so need to do the extra work to parse the AST and examine it properly.
acornWalk.simple(acorn.parse(functionText,{ecmaVersion: "latest"} ), {
CallExpression(astNode) {
if (astNode.callee && astNode.callee.object) {
if (astNode.callee.object.name === "node" && astNode.callee.property.name === "done") {
handleNodeDoneCall = false;
}
}
}
})
}
var finScript = null;
var finOpt = null;
node.topic = n.topic;
node.outstandingTimers = [];
node.outstandingIntervals = [];
node.clearStatus = false;
var sandbox = {
console:console,
util:util,
Buffer:Buffer,
Date: Date,
RED: {
util: RED.util
},
__node__: {
id: node.id,
name: node.name,
outputCount: node.outputs,
log: function() {
node.log.apply(node, arguments);
},
error: function() {
node.error.apply(node, arguments);
},
warn: function() {
node.warn.apply(node, arguments);
},
debug: function() {
node.debug.apply(node, arguments);
},
trace: function() {
node.trace.apply(node, arguments);
},
send: function(send, id, msgs, cloneMsg) {
sendResults(node, send, id, msgs, cloneMsg);
},
on: function() {
if (arguments[0] === "input") {
throw new Error(RED._("function.error.inputListener"));
}
node.on.apply(node, arguments);
},
status: function() {
node.clearStatus = true;
node.status.apply(node, arguments);
}
},
context: {
set: function() {
node.context().set.apply(node,arguments);
},
get: function() {
return node.context().get.apply(node,arguments);
},
keys: function() {
return node.context().keys.apply(node,arguments);
},
get global() {
return node.context().global;
},
get flow() {
return node.context().flow;
}
},
flow: {
set: function() {
node.context().flow.set.apply(node,arguments);
},
get: function() {
return node.context().flow.get.apply(node,arguments);
},
keys: function() {
return node.context().flow.keys.apply(node,arguments);
}
},
global: {
set: function() {
node.context().global.set.apply(node,arguments);
},
get: function() {
return node.context().global.get.apply(node,arguments);
},
keys: function() {
return node.context().global.keys.apply(node,arguments);
}
},
env: {
get: function(envVar) {
return RED.util.getSetting(node, envVar);
}
},
setTimeout: function () {
var func = arguments[0];
var timerId;
arguments[0] = function() {
sandbox.clearTimeout(timerId);
try {
func.apply(node,arguments);
} catch(err) {
node.error(err,{});
}
};
timerId = setTimeout.apply(node,arguments);
node.outstandingTimers.push(timerId);
return timerId;
},
clearTimeout: function(id) {
clearTimeout(id);
var index = node.outstandingTimers.indexOf(id);
if (index > -1) {
node.outstandingTimers.splice(index,1);
}
},
setInterval: function() {
var func = arguments[0];
var timerId;
arguments[0] = function() {
try {
func.apply(node,arguments);
} catch(err) {
node.error(err,{});
}
};
timerId = setInterval.apply(node,arguments);
node.outstandingIntervals.push(timerId);
return timerId;
},
clearInterval: function(id) {
clearInterval(id);
var index = node.outstandingIntervals.indexOf(id);
if (index > -1) {
node.outstandingIntervals.splice(index,1);
}
}
};
if (util.hasOwnProperty('promisify')) {
sandbox.setTimeout[util.promisify.custom] = function(after, value) {
return new Promise(function(resolve, reject) {
sandbox.setTimeout(function(){ resolve(value); }, after);
});
};
sandbox.promisify = util.promisify;
}
const moduleLoadPromises = [];
if (node.hasOwnProperty("libs")) {
let moduleErrors = false;
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
if (sandbox.hasOwnProperty(vname) || vname === 'node') {
node.error(RED._("function.error.moduleNameError",{name:vname}))
moduleErrors = true;
return;
}
sandbox[vname] = null;
var spec = module.module;
if (spec && (spec !== "")) {
moduleLoadPromises.push(RED.import(module.module).then(lib => {
sandbox[vname] = lib.default;
}).catch(err => {
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()}))
throw err;
}));
}
}
});
if (moduleErrors) {
throw new Error(RED._("function.error.externalModuleLoadError"));
}
}
const RESOLVING = 0;
const RESOLVED = 1;
const ERROR = 2;
var state = RESOLVING;
var messages = [];
var processMessage = (() => {});
node.on("input", function(msg,send,done) {
if(state === RESOLVING) {
messages.push({msg:msg, send:send, done:done});
}
else if(state === RESOLVED) {
processMessage(msg, send, done);
}
});
Promise.all(moduleLoadPromises).then(() => {
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
(async function(__send__) {
var node = {
id:__node__.id,
name:__node__.name,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), msgs, cloneMsg);
}
};
`+ node.ini +`
})(__initSend__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = `(function () {
var node = {
id:__node__.id,
name:__node__.name,
outputCount:__node__.outputCount,
log:__node__.log,
error:__node__.error,
warn:__node__.warn,
debug:__node__.debug,
trace:__node__.trace,
status:__node__.status,
send: function(msgs, cloneMsg) {
__node__.error("Cannot send from close function");
}
};
`+node.fin +`
})();`;
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
}
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(err => {
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
//remove unwanted part
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
var stack = err.stack.split(/\r?\n/);
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
if (line < stack.length) {
errorMessage = stack[line];
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
if (m) {
var lineno = Number(m[1])-1;
var cha = m[2];
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
}
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
});
}
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
}).catch(err => {
node.error(RED._("function.error.externalModuleLoadError"));
});
}
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: true, exportable: true }
}
});
RED.library.register("functions");
};

View File

@ -0,0 +1,284 @@
<!--
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.
-->
<script type="text/html" data-template-name="delay">
<div class="form-row">
<label for="node-input-delay-action"><i class="fa fa-tasks"></i> <span data-i18n="delay.action"></span></label>
<select id="node-input-delay-action" style="width:270px !important">
<option value="delay" data-i18n="delay.delaymsg"></option>
<option value="rate" data-i18n="delay.limitrate"></option>
</select>
</div>
<div id="delay-details">
<div class="form-row">
<label></label>
<select id="node-input-delay-type" style="width:270px !important">
<option value="delay" data-i18n="delay.delayfixed"></option>
<option value="random" data-i18n="delay.randomdelay"></option>
<option value="delayv" data-i18n="delay.delayvarmsg"></option>
</select>
</div>
<div class="form-row" id="delay-details-for">
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="delay.for"></span></label>
<input type="text" id="node-input-timeout" style="text-align:end; width:50px !important">
<select id="node-input-timeoutUnits" style="width:200px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
<option value="minutes" data-i18n="delay.mins"></option>
<option value="hours" data-i18n="delay.hours"></option>
<option value="days" data-i18n="delay.days"></option>
</select>
</div>
<div id="random-details" class="form-row">
<label for="node-input-randomFirst"><i class="fa fa-clock-o"></i> <span data-i18n="delay.between"></span></label>
<input type="text" id="node-input-randomFirst" placeholder="" style="text-align:end; width:50px !important">
&nbsp;<span data-i18n="delay.and"></span>&nbsp;
<input type="text" id="node-input-randomLast" placeholder="" style="text-align:end; width:50px !important">
<select id="node-input-randomUnits" style="width:140px !important">
<option value="milliseconds" data-i18n="delay.milisecs"></option>
<option value="seconds" data-i18n="delay.secs"></option>
<option value="minutes" data-i18n="delay.mins"></option>
<option value="hours" data-i18n="delay.hours"></option>
<option value="days" data-i18n="delay.days"></option>
</select>
</div>
</div>
<div id="rate-details">
<div class="form-row">
<label></label>
<select id="node-input-rate-type" style="width:270px !important">
<option value="all" data-i18n="delay.limitall"></option>
<option value="topic" data-i18n="delay.limittopic"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-rate"><i class="fa fa-clock-o"></i> <span data-i18n="delay.rate"></span></label>
<input type="text" id="node-input-rate" placeholder="1" style="text-align:end; width:40px !important">
<label for="node-input-rateUnits"><span data-i18n="delay.msgper"></span></label>
<input type="text" id="node-input-nbRateUnits" placeholder="1" style="text-align:end; width:40px !important">
<select id="node-input-rateUnits" style="width:90px !important">
<option value="second" data-i18n="delay.label.units.second.singular"></option>
<option value="minute" data-i18n="delay.label.units.minute.singular"></option>
<option value="hour" data-i18n="delay.label.units.hour.singular"></option>
<option value="day" data-i18n="delay.label.units.day.singular"></option>
</select>
</div>
<div class="form-row" id="rate-override" style="display: flex; align-items: center">
<label></label><input style="width:30px; margin:0" type="checkbox" id="node-input-allowrate"><label style="margin:0;width: auto;" for="node-input-allowrate" data-i18n="delay.allowrate"></label>
</div>
<div class="form-row" id="rate-details-drop">
<input type="hidden" id="node-input-outputs" value="1">
<label></label>
<select id="node-input-drop-select" style="width: 70%">
<option id="node-input-drop-select-queue" value="queue" data-i18n="delay.queuemsg"></option>
<option value="drop" data-i18n="delay.dropmsg"></option>
<option value="emit" data-i18n="delay.sendmsg"></option>
</select>
</div>
<div class="form-row" id="rate-details-per-topic">
<label></label>
<select id="node-input-rate-topic-type" style="width:270px !important">
<option value="queue" data-i18n="delay.fairqueue"></option>
<option value="timed" data-i18n="delay.timedqueue"></option>
</select>
</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">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('delay',{
category: 'function',
color:"#E6E0F8",
defaults: {
name: {value:""},
pauseType: {value:"delay", required:true},
timeout: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
timeoutUnits: {value:"seconds"},
rate: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
nbRateUnits: {value:"1", required:false,
validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
rateUnits: {value: "second"},
randomFirst: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomLast: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }},
randomUnits: {value: "seconds"},
drop: {value:false},
allowrate: {value:false},
outputs: { value: 1},
},
inputs:1,
outputs:1,
icon: "timer.svg",
label: function() {
if (this.name) {
return this.name;
}
if (this.pauseType == "delayv") {
return this._("delay.label.variable");
} else if (this.pauseType == "delay") {
var units = this.timeoutUnits ? this.timeoutUnits.charAt(0) : "s";
if (this.timeoutUnits == "milliseconds") { units = "ms"; }
return this._("delay.label.delay")+" "+this.timeout+units;
} else if (this.pauseType == "random") {
return this._("delay.label.random");
} else {
var rate = this.rate+" msg/"+(this.rateUnits ? (this.nbRateUnits > 1 ? this.nbRateUnits : '') + this.rateUnits.charAt(0) : "s");
if (this.pauseType == "rate") {
return this._("delay.label.limit")+" "+rate;
} else if (this.pauseType == "timed") {
return this._("delay.label.limitTopic")+" "+rate;
} else {
return this._("delay.label.limitTopic")+" "+rate;
}
}
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
$( "#node-input-timeout" ).spinner({min:1});
$( "#node-input-rate" ).spinner({min:1});
$( "#node-input-nbRateUnits" ).spinner({min:1});
$( "#node-input-randomFirst" ).spinner({min:0});
$( "#node-input-randomLast" ).spinner({min:1});
$('.ui-spinner-button').on("click", function() {
$(this).siblings('input').trigger("change");
});
$( "#node-input-nbRateUnits" ).on('change keyup', function() {
var $this = $(this);
var val = parseInt($this.val());
var type = "singular";
if (val > 1) {
type = "plural";
}
if ($this.attr("data-type") == type) {
return;
}
$this.attr("data-type", type);
$("#node-input-rateUnits option").each(function () {
var $option = $(this);
var key = "delay.label.units." + $option.val() + "." + type;
$option.attr('data-i18n', 'node-red:' + key);
$option.html(node._(key));
});
});
if (this.pauseType == "delay") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('delay');
} else if (this.pauseType == "delayv") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('delayv');
} else if (this.pauseType == "random") {
$("#node-input-delay-action").val('delay');
$("#node-input-delay-type").val('random');
} else if (this.pauseType == "rate") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('all');
} else if (this.pauseType == "queue") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('topic');
$("#node-input-rate-topic-type").val('queue');
} else if (this.pauseType == "timed") {
$("#node-input-delay-action").val('rate');
$("#node-input-rate-type").val('topic');
$("#node-input-rate-topic-type").val('timed');
}
if (!this.timeoutUnits) {
$("#node-input-timeoutUnits option").filter(function() {
return $(this).val() == 'seconds';
}).attr('selected', true);
}
if (!this.randomUnits) {
$("#node-input-randomUnits option").filter(function() {
return $(this).val() == 'seconds';
}).attr('selected', true);
}
$("#node-input-delay-action").on("change",function() {
if (this.value === "delay") {
$("#delay-details").show();
$("#rate-details").hide();
} else if (this.value === "rate") {
$("#delay-details").hide();
$("#rate-details").show();
}
}).trigger("change");
$("#node-input-delay-type").on("change", function() {
if (this.value === "delay") {
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "delayv") {
$("#delay-details-for").show();
$("#random-details").hide();
} else if (this.value === "random") {
$("#delay-details-for").hide();
$("#random-details").show();
}
}).trigger("change");
if (this.outputs === 2) {
$("#node-input-drop-select").val("emit");
} else if (this.drop) {
$("#node-input-drop-select").val("drop");
} else {
$("#node-input-drop-select").val("queue");
}
$("#node-input-rate-type").on("change", function() {
if (this.value === "all") {
$("#rate-details-per-topic").hide();
$("#node-input-drop-select-queue").attr('disabled', false);
} else if (this.value === "topic") {
if ($("#node-input-drop-select").val() === "queue") {
$("#node-input-drop-select").val("drop");
}
$("#node-input-drop-select-queue").attr('disabled', true);
$("#rate-details-per-topic").show();
}
}).trigger("change");
},
oneditsave: function() {
var action = $("#node-input-delay-action").val();
if (action === "delay") {
this.pauseType = $("#node-input-delay-type").val();
$("#node-input-outputs").val(1);
} else if (action === "rate") {
action = $("#node-input-rate-type").val();
if (action === "all") {
this.pauseType = "rate";
} else {
this.pauseType = $("#node-input-rate-topic-type").val();
}
var dropType = $("#node-input-drop-select").val();
this.drop = dropType !== "queue";
$("#node-input-outputs").val(dropType === "emit"?2:1);
}
}
});
</script>

View File

@ -0,0 +1,97 @@
module.exports = function(RED) {
"use strict";
function RbeNode(n) {
RED.nodes.createNode(this,n);
this.func = n.func || "rbe";
this.gap = n.gap || "0";
this.start = n.start || '';
this.inout = n.inout || "out";
this.pc = false;
if (this.gap.substr(-1) === "%") {
this.pc = true;
this.gap = parseFloat(this.gap);
}
this.g = this.gap;
this.property = n.property || "payload";
this.topi = n.topi || "topic";
this.septopics = true;
if (n.septopics !== undefined && n.septopics === false) {
this.septopics = false;
}
var node = this;
node.previous = {};
this.on("input",function(msg) {
var topic;
try {
topic = RED.util.getMessageProperty(msg,node.topi);
}
catch(e) { }
if (msg.hasOwnProperty("reset")) {
if (node.septopics && topic && (typeof topic === "string") && (topic !== "")) {
delete node.previous[msg.topic];
}
else { node.previous = {}; }
}
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
var t = "_no_topic";
if (node.septopics) { t = topic || t; }
if ((this.func === "rbe") || (this.func === "rbei")) {
var doSend = (this.func !== "rbei") || (node.previous.hasOwnProperty(t)) || false;
if (typeof(value) === "object") {
if (typeof(node.previous[t]) !== "object") { node.previous[t] = {}; }
if (!RED.util.compareObjects(value, node.previous[t])) {
node.previous[t] = RED.util.cloneMessage(value);
if (doSend) { node.send(msg); }
}
}
else {
if (value !== node.previous[t]) {
node.previous[t] = RED.util.cloneMessage(value);
if (doSend) { node.send(msg); }
}
}
}
else {
var n = parseFloat(value);
if (!isNaN(n)) {
if ((typeof node.previous[t] === 'undefined') && (this.func === "narrowband" || this.func === "narrowbandEq")) {
if (node.start === '') { node.previous[t] = n; }
else { node.previous[t] = node.start; }
}
if (node.pc) { node.gap = Math.abs(node.previous[t] * node.g / 100) || 0; }
else { node.gap = Number(node.gap); }
if ((node.previous[t] === undefined) && (node.func === "narrowbandEq")) { node.previous[t] = n; }
if (node.previous[t] === undefined) { node.previous[t] = n - node.gap - 1; }
if (Math.abs(n - node.previous[t]) === node.gap) {
if ((this.func === "deadbandEq")||(this.func === "narrowband")) {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
else if (Math.abs(n - node.previous[t]) > node.gap) {
if (this.func === "deadband" || this.func === "deadbandEq") {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
else if (Math.abs(n - node.previous[t]) < node.gap) {
if ((this.func === "narrowband")||(this.func === "narrowbandEq")) {
if (node.inout === "out") { node.previous[t] = n; }
node.send(msg);
}
}
if (node.inout === "in") { node.previous[t] = n; }
}
else {
node.warn(RED._("rbe.warn.nonumber"));
}
}
} // ignore msg with no payload property.
});
}
RED.nodes.registerType("rbe",RbeNode);
}

View File

@ -0,0 +1,675 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
const got = require("got");
const {CookieJar} = require("tough-cookie");
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const FormData = require('form-data');
const { v4: uuid } = require('uuid');
const crypto = require('crypto');
const URL = require("url").URL
var mustache = require("mustache");
var querystring = require("querystring");
var cookie = require("cookie");
var hashSum = require("hash-sum");
// Cache a reference to the existing https.request function
// so we can compare later to see if an old agent-base instance
// has been required.
// This is generally okay as the core nodes are required before
// any contrib nodes. Where it will fail is if the agent-base module
// is required via the settings file or outside of Node-RED before it
// is started.
// If there are other modules that patch the function, they will get undone
// as well. Not much we can do about that right now. Patching core
// functions is bad.
const HTTPS_MODULE = require("https");
const HTTPS_REQUEST = HTTPS_MODULE.request;
function checkNodeAgentPatch() {
if (HTTPS_MODULE.request !== HTTPS_REQUEST && HTTPS_MODULE.request.length === 2) {
RED.log.warn(`
---------------------------------------------------------------------
Patched https.request function detected. This will break the
HTTP Request node. The original code has now been restored.
This is likely caused by a contrib node including an old version of
the 'agent-base@<5.0.0' module.
You can identify what node is at fault by running:
npm list agent-base
in your Node-RED user directory (${RED.settings.userDir}).
---------------------------------------------------------------------
`);
HTTPS_MODULE.request = HTTPS_REQUEST
}
}
function HTTPRequest(n) {
RED.nodes.createNode(this,n);
checkNodeAgentPatch();
var node = this;
var nodeUrl = n.url;
var isTemplatedUrl = (nodeUrl||"").indexOf("{{") != -1;
var nodeMethod = n.method || "GET";
var paytoqs = false;
var paytobody = false;
var redirectList = [];
var sendErrorsToCatch = n.senderr;
var nodeHTTPPersistent = n["persist"];
if (n.tls) {
var tlsNode = RED.nodes.getNode(n.tls);
}
this.ret = n.ret || "txt";
this.authType = n.authType || "basic";
if (RED.settings.httpRequestTimeout) { this.reqTimeout = parseInt(RED.settings.httpRequestTimeout) || 120000; }
else { this.reqTimeout = 120000; }
if (n.paytoqs === true || n.paytoqs === "query") { paytoqs = true; }
else if (n.paytoqs === "body") { paytobody = true; }
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var proxyConfig = null;
if (n.proxy) {
proxyConfig = RED.nodes.getNode(n.proxy);
prox = proxyConfig.url;
noprox = proxyConfig.noproxy;
}
let timingLog = false;
if (RED.settings.hasOwnProperty("httpRequestTimingLog")) {
timingLog = RED.settings.httpRequestTimingLog;
}
this.on("input",function(msg,nodeSend,nodeDone) {
checkNodeAgentPatch();
//reset redirectList on each request
redirectList = [];
var preRequestTimestamp = process.hrtime();
node.status({fill:"blue",shape:"dot",text:"httpin.status.requesting"});
var url = nodeUrl || msg.url;
if (msg.url && nodeUrl && (nodeUrl !== msg.url)) { // revert change below when warning is finally removed
node.warn(RED._("common.errors.nooverride"));
}
if (isTemplatedUrl) {
url = mustache.render(nodeUrl,msg);
}
if (!url) {
node.error(RED._("httpin.errors.no-url"),msg);
nodeDone();
return;
}
// url must start http:// or https:// so assume http:// if not set
if (url.indexOf("://") !== -1 && url.indexOf("http") !== 0) {
node.warn(RED._("httpin.errors.invalid-transport"));
node.status({fill:"red",shape:"ring",text:"httpin.errors.invalid-transport"});
nodeDone();
return;
}
if (!((url.indexOf("http://") === 0) || (url.indexOf("https://") === 0))) {
if (tlsNode) {
url = "https://"+url;
} else {
url = "http://"+url;
}
}
// The Request module used in Node-RED 1.x was tolerant of query strings that
// were partially encoded. For example - "?a=hello%20there&b=20%"
// The GOT module doesn't like that.
// The following is an attempt to normalise the url to ensure it is properly
// encoded. We cannot just encode it directly as we don't want any valid
// encoded entity to end up doubly encoded.
if (url.indexOf("?") > -1) {
// Only do this if there is a query string to deal with
const [hostPath, ...queryString] = url.split("?")
const query = queryString.join("?");
if (query) {
// Look for any instance of % not followed by two hex chars.
// Replace any we find with %25.
const escapedQueryString = query.replace(/(%.?.?)/g, function(v) {
if (/^%[a-f0-9]{2}/i.test(v)) {
return v;
}
return v.replace(/%/,"%25")
})
url = hostPath+"?"+escapedQueryString;
}
}
var method = nodeMethod.toUpperCase() || "GET";
if (msg.method && n.method && (n.method !== "use")) { // warn if override option not set
node.warn(RED._("common.errors.nooverride"));
}
if (msg.method && n.method && (n.method === "use")) {
method = msg.method.toUpperCase(); // use the msg parameter
}
// var isHttps = (/^https/i.test(url));
var opts = {};
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
// Had to remove this to get http->https redirect to work
// opts.defaultPort = isHttps?443:80;
opts.timeout = node.reqTimeout;
opts.throwHttpErrors = false;
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false;
opts.method = method;
opts.headers = {};
opts.retry = 0;
opts.responseType = 'buffer';
opts.maxRedirects = 21;
opts.cookieJar = new CookieJar();
opts.ignoreInvalidCookies = true;
opts.forever = nodeHTTPPersistent;
if (msg.requestTimeout !== undefined) {
if (isNaN(msg.requestTimeout)) {
node.warn(RED._("httpin.errors.timeout-isnan"));
} else if (msg.requestTimeout < 1) {
node.warn(RED._("httpin.errors.timeout-isnegative"));
} else {
opts.timeout = msg.requestTimeout;
}
}
const originalHeaderMap = {};
opts.hooks = {
beforeRequest: [
options => {
// Whilst HTTP headers are meant to be case-insensitive,
// in the real world, there are servers that aren't so compliant.
// GOT will lower case all headers given a chance, so we need
// to restore the case of any headers the user has set.
Object.keys(options.headers).forEach(h => {
if (originalHeaderMap[h] && originalHeaderMap[h] !== h) {
options.headers[originalHeaderMap[h]] = options.headers[h];
delete options.headers[h];
}
})
}
],
beforeRedirect: [
(options, response) => {
let redirectInfo = {
location: response.headers.location
}
if (response.headers.hasOwnProperty('set-cookie')) {
redirectInfo.cookies = extractCookies(response.headers['set-cookie']);
}
redirectList.push(redirectInfo)
}
]
}
var ctSet = "Content-Type"; // set default camel case
var clSet = "Content-Length";
if (msg.headers) {
if (msg.headers.hasOwnProperty('x-node-red-request-node')) {
var headerHash = msg.headers['x-node-red-request-node'];
delete msg.headers['x-node-red-request-node'];
var hash = hashSum(msg.headers);
if (hash === headerHash) {
delete msg.headers;
}
}
if (msg.headers) {
for (var v in msg.headers) {
if (msg.headers.hasOwnProperty(v)) {
var name = v.toLowerCase();
if (name !== "content-type" && name !== "content-length") {
// only normalise the known headers used later in this
// function. Otherwise leave them alone.
name = v;
}
else if (name === 'content-type') { ctSet = v; }
else { clSet = v; }
opts.headers[name] = msg.headers[v];
}
}
}
}
if (msg.hasOwnProperty('followRedirects')) {
opts.followRedirect = !!msg.followRedirects;
}
if (opts.headers.hasOwnProperty('cookie')) {
var cookies = cookie.parse(opts.headers.cookie, {decode:String});
for (var name in cookies) {
opts.cookieJar.setCookie(cookie.serialize(name, cookies[name], {encode:String}), url, {ignoreError: true});
}
delete opts.headers.cookie;
}
if (msg.cookies) {
for (var name in msg.cookies) {
if (msg.cookies.hasOwnProperty(name)) {
if (msg.cookies[name] === null || msg.cookies[name].value === null) {
// This case clears a cookie for HTTP In/Response nodes.
// Ignore for this node.
} else if (typeof msg.cookies[name] === 'object') {
if(msg.cookies[name].encode === false){
// If the encode option is false, the value is not encoded.
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value, {encode: String}), url, {ignoreError: true});
} else {
// The value is encoded by encodeURIComponent().
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name].value), url, {ignoreError: true});
}
} else {
opts.cookieJar.setCookie(cookie.serialize(name, msg.cookies[name]), url, {ignoreError: true});
}
}
}
}
var parsedURL = new URL(url)
this.credentials = this.credentials || {}
if (parsedURL.username && !this.credentials.user) {
this.credentials.user = parsedURL.username
}
if (parsedURL.password && !this.credentials.password) {
this.credentials.password = parsedURL.password
}
if (Object.keys(this.credentials).length != 0) {
if (this.authType === "basic") {
// Workaround for https://github.com/sindresorhus/got/issues/1169 (fixed in got v12)
// var cred = ""
if (this.credentials.user || this.credentials.password) {
// cred = `${this.credentials.user}:${this.credentials.password}`;
if (this.credentials.user === undefined) { this.credentials.user = ""}
if (this.credentials.password === undefined) { this.credentials.password = ""}
opts.headers.Authorization = "Basic " + Buffer.from(`${this.credentials.user}:${this.credentials.password}`).toString("base64");
}
// build own basic auth header
// opts.headers.Authorization = "Basic " + Buffer.from(cred).toString("base64");
} else if (this.authType === "digest") {
let digestCreds = this.credentials;
let sentCreds = false;
opts.hooks.afterResponse = [(response, retry) => {
if (response.statusCode === 401) {
if (sentCreds) {
return response
}
const requestUrl = new URL(response.request.requestUrl);
const options = response.request.options;
const normalisedHeaders = {};
Object.keys(response.headers).forEach(k => {
normalisedHeaders[k.toLowerCase()] = response.headers[k]
})
if (normalisedHeaders['www-authenticate']) {
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader;
}
sentCreds = true;
return retry(options);
}
return response
}];
} else if (this.authType === "bearer") {
opts.headers.Authorization = `Bearer ${this.credentials.password||""}`
}
}
var payload = null;
if (method !== 'GET' && method !== 'HEAD' && typeof msg.payload !== "undefined") {
if (opts.headers['content-type'] == 'multipart/form-data' && typeof msg.payload === "object") {
let formData = new FormData();
for (var opt in msg.payload) {
if (msg.payload.hasOwnProperty(opt)) {
var val = msg.payload[opt];
if (val !== undefined && val !== null) {
if (typeof val === 'string' || Buffer.isBuffer(val)) {
formData.append(opt, val);
} else if (typeof val === 'object' && val.hasOwnProperty('value')) {
formData.append(opt,val.value,val.options || {});
} else {
formData.append(opt,JSON.stringify(val));
}
}
}
}
// GOT will only set the content-type header with the correct boundary
// if the header isn't set. So we delete it here, for GOT to reset it.
delete opts.headers['content-type'];
opts.body = formData;
} else {
if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
payload = msg.payload;
} else if (typeof msg.payload == "number") {
payload = msg.payload+"";
} else {
if (opts.headers['content-type'] == 'application/x-www-form-urlencoded') {
payload = querystring.stringify(msg.payload);
} else {
payload = JSON.stringify(msg.payload);
if (opts.headers['content-type'] == null) {
opts.headers[ctSet] = "application/json";
}
}
}
if (opts.headers['content-length'] == null) {
if (Buffer.isBuffer(payload)) {
opts.headers[clSet] = payload.length;
} else {
opts.headers[clSet] = Buffer.byteLength(payload);
}
}
opts.body = payload;
}
}
if (method == 'GET' && typeof msg.payload !== "undefined" && paytoqs) {
if (typeof msg.payload === "object") {
try {
if (url.indexOf("?") !== -1) {
url += (url.endsWith("?")?"":"&") + querystring.stringify(msg.payload);
} else {
url += "?" + querystring.stringify(msg.payload);
}
} catch(err) {
node.error(RED._("httpin.errors.invalid-payload"),msg);
nodeDone();
return;
}
} else {
node.error(RED._("httpin.errors.invalid-payload"),msg);
nodeDone();
return;
}
} else if ( method == "GET" && typeof msg.payload !== "undefined" && paytobody) {
opts.allowGetBody = true;
if (typeof msg.payload === "object") {
opts.body = JSON.stringify(msg.payload);
} else if (typeof msg.payload == "number") {
opts.body = msg.payload+"";
} else if (typeof msg.payload === "string" || Buffer.isBuffer(msg.payload)) {
opts.body = msg.payload;
}
}
// revert to user supplied Capitalisation if needed.
if (opts.headers.hasOwnProperty('content-type') && (ctSet !== 'content-type')) {
opts.headers[ctSet] = opts.headers['content-type'];
delete opts.headers['content-type'];
}
if (opts.headers.hasOwnProperty('content-length') && (clSet !== 'content-length')) {
opts.headers[clSet] = opts.headers['content-length'];
delete opts.headers['content-length'];
}
var noproxy;
if (noprox) {
for (var i = 0; i < noprox.length; i += 1) {
if (url.indexOf(noprox[i]) !== -1) { noproxy=true; }
}
}
if (prox && !noproxy) {
var match = prox.match(/^(https?:\/\/)?(.+)?:([0-9]+)?/i);
if (match) {
let proxyAgent;
let proxyURL = new URL(prox);
//set username/password to null to stop empty creds header
let proxyOptions = {
proxy: {
protocol: proxyURL.protocol,
hostname: proxyURL.hostname,
port: proxyURL.port,
username: null,
password: null
},
maxFreeSockets: 256,
maxSockets: 256,
keepAlive: true
}
if (proxyConfig && proxyConfig.credentials) {
let proxyUsername = proxyConfig.credentials.username || '';
let proxyPassword = proxyConfig.credentials.password || '';
if (proxyUsername || proxyPassword) {
proxyOptions.proxy.username = proxyUsername;
proxyOptions.proxy.password = proxyPassword;
}
} else if (proxyURL.username || proxyURL.password){
proxyOptions.proxy.username = proxyURL.username;
proxyOptions.proxy.password = proxyURL.password;
}
//need both incase of http -> https redirect
opts.agent = {
http: new HttpProxyAgent(proxyOptions),
https: new HttpsProxyAgent(proxyOptions)
};
} else {
node.warn("Bad proxy url: "+ prox);
}
}
if (tlsNode) {
opts.https = {};
tlsNode.addTLSOptions(opts.https);
if (opts.https.ca) {
opts.https.certificateAuthority = opts.https.ca;
delete opts.https.ca;
}
if (opts.https.cert) {
opts.https.certificate = opts.https.cert;
delete opts.https.cert;
}
} else {
if (msg.hasOwnProperty('rejectUnauthorized')) {
opts.https = { rejectUnauthorized: msg.rejectUnauthorized };
}
}
// Now we have established all of our own headers, take a snapshot
// of their case so we can restore it prior to the request being sent.
if (opts.headers) {
Object.keys(opts.headers).forEach(h => {
originalHeaderMap[h.toLowerCase()] = h
})
}
got(url,opts).then(res => {
msg.statusCode = res.statusCode;
msg.headers = res.headers;
msg.responseUrl = res.url;
msg.payload = res.body;
msg.redirectList = redirectList;
msg.retry = 0;
if (msg.headers.hasOwnProperty('set-cookie')) {
msg.responseCookies = extractCookies(msg.headers['set-cookie']);
}
msg.headers['x-node-red-request-node'] = hashSum(msg.headers);
// msg.url = url; // revert when warning above finally removed
if (node.metric()) {
// Calculate request time
var diff = process.hrtime(preRequestTimestamp);
var ms = diff[0] * 1e3 + diff[1] * 1e-6;
var metricRequestDurationMillis = ms.toFixed(3);
node.metric("duration.millis", msg, metricRequestDurationMillis);
if (res.client && res.client.bytesRead) {
node.metric("size.bytes", msg, res.client.bytesRead);
}
if (timingLog) {
emitTimingMetricLog(res.timings, msg);
}
}
// Convert the payload to the required return type
if (node.ret !== "bin") {
msg.payload = msg.payload.toString('utf8'); // txt
if (node.ret === "obj") {
try { msg.payload = JSON.parse(msg.payload); } // obj
catch(e) { node.warn(RED._("httpin.errors.json-error")); }
}
}
node.status({});
nodeSend(msg);
nodeDone();
}).catch(err => {
// Pre 2.1, any errors would be sent to both Catch node and sent on as normal.
// This is not ideal but is the legacy behaviour of the node.
// 2.1 adds the 'senderr' option, if set to true, will *only* send errors
// to Catch nodes. If false, it still does both behaviours.
// TODO: 3.0 - make it one or the other.
if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
node.error(RED._("common.notification.errors.no-response"), msg);
node.status({fill:"red", shape:"ring", text:"common.notification.errors.no-response"});
} else {
node.error(err,msg);
node.status({fill:"red", shape:"ring", text:err.code});
}
msg.payload = err.toString() + " : " + url;
msg.statusCode = err.code || (err.response?err.response.statusCode:undefined);
if (node.metric() && timingLog) {
emitTimingMetricLog(err.timings, msg);
}
if (!sendErrorsToCatch) {
nodeSend(msg);
}
nodeDone();
});
});
this.on("close",function() {
node.status({});
});
function emitTimingMetricLog(timings, msg) {
const props = [
"start",
"socket",
"lookup",
"connect",
"secureConnect",
"upload",
"response",
"end",
"error",
"abort"
];
if (timings) {
props.forEach(p => {
if (timings[p]) {
node.metric(`timings.${p}`, msg, timings[p]);
}
});
}
}
function extractCookies(setCookie) {
var cookies = {};
setCookie.forEach(function(c) {
var parsedCookie = cookie.parse(c);
var eq_idx = c.indexOf('=');
var key = c.substr(0, eq_idx).trim()
parsedCookie.value = parsedCookie[key];
delete parsedCookie[key];
cookies[key] = parsedCookie;
});
return cookies;
}
}
RED.nodes.registerType("http request",HTTPRequest,{
credentials: {
user: {type:"text"},
password: {type: "password"}
}
});
const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') }
function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
/**
* RFC 2617: handle both MD5 and MD5-sess algorithms.
*
* If the algorithm directive's value is "MD5" or unspecified, then HA1 is
* HA1=MD5(username:realm:password)
* If the algorithm directive's value is "MD5-sess", then HA1 is
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
*/
var ha1 = md5(user + ':' + realm + ':' + pass)
if (algorithm && algorithm.toLowerCase() === 'md5-sess') {
return md5(ha1 + ':' + nonce + ':' + cnonce)
} else {
return ha1
}
}
function buildDigestHeader(user, pass, method, path, authHeader) {
var challenge = {}
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
for (;;) {
var match = re.exec(authHeader)
if (!match) {
break
}
challenge[match[1]] = match[2] || match[3]
}
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
var nc = qop && '00000001'
var cnonce = qop && uuid().replace(/-/g, '')
var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce)
var ha2 = md5(method + ':' + path)
var digestResponse = qop
? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
: md5(ha1 + ':' + challenge.nonce + ':' + ha2)
var authValues = {
username: user,
realm: challenge.realm,
nonce: challenge.nonce,
uri: path,
qop: qop,
response: digestResponse,
nc: nc,
cnonce: cnonce,
algorithm: challenge.algorithm,
opaque: challenge.opaque
}
authHeader = []
for (var k in authValues) {
if (authValues[k]) {
if (k === 'qop' || k === 'nc' || k === 'algorithm') {
authHeader.push(k + '=' + authValues[k])
} else {
authHeader.push(k + '="' + authValues[k] + '"')
}
}
}
authHeader = 'Digest ' + authHeader.join(', ')
return authHeader
}
}

View File

@ -0,0 +1,417 @@
/**
* 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.
**/
module.exports = function(RED) {
"use strict";
var ws = require("ws");
var inspect = require("util").inspect;
var url = require("url");
var HttpsProxyAgent = require('https-proxy-agent');
var serverUpgradeAdded = false;
function handleServerUpgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
if (listenerNodes.hasOwnProperty(pathname)) {
listenerNodes[pathname].server.handleUpgrade(request, socket, head, function done(ws) {
listenerNodes[pathname].server.emit('connection', ws, request);
});
} else {
// Don't destroy the socket as other listeners may want to handle the
// event.
}
}
var listenerNodes = {};
var activeListenerNodes = 0;
// A node red node that sets up a local websocket server
function WebSocketListenerNode(n) {
// Create a RED node
RED.nodes.createNode(this,n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
node.path = n.path;
node.wholemsg = (n.wholemsg === "true");
node._inputNodes = []; // collection of nodes that want to receive events
node._clients = {};
// match absolute url
node.isServer = !/^ws{1,2}:\/\//i.test(node.path);
node.closing = false;
node.tls = n.tls;
if (n.hb) {
var heartbeat = parseInt(n.hb);
if (heartbeat > 0) {
node.heartbeat = heartbeat * 1000;
}
}
function startconn() { // Connect to remote endpoint
node.tout = null;
var prox, noprox;
if (process.env.http_proxy) { prox = process.env.http_proxy; }
if (process.env.HTTP_PROXY) { prox = process.env.HTTP_PROXY; }
if (process.env.no_proxy) { noprox = process.env.no_proxy.split(","); }
if (process.env.NO_PROXY) { noprox = process.env.NO_PROXY.split(","); }
var noproxy = false;
if (noprox) {
for (var i in noprox) {
if (node.path.indexOf(noprox[i].trim()) !== -1) { noproxy=true; }
}
}
var agent = undefined;
if (prox && !noproxy) {
agent = new HttpsProxyAgent(prox);
}
var options = {};
if (agent) {
options.agent = agent;
}
if (node.tls) {
var tlsNode = RED.nodes.getNode(node.tls);
if (tlsNode) {
tlsNode.addTLSOptions(options);
}
}
var socket = new ws(node.path,options);
socket.setMaxListeners(0);
node.server = socket; // keep for closing
handleConnection(socket);
}
function handleConnection(/*socket*/socket) {
var id = RED.util.generateId();
socket.nrId = id;
socket.nrPendingHeartbeat = false;
if (node.isServer) {
node._clients[id] = socket;
node.emit('opened',{count:Object.keys(node._clients).length,id:id});
}
socket.on('open',function() {
if (!node.isServer) {
if (node.heartbeat) {
clearInterval(node.heartbeatInterval);
node.heartbeatInterval = setInterval(function() {
if (socket.nrPendingHeartbeat) {
// No pong received
socket.terminate();
socket.nrErrorHandler(new Error("timeout"));
return;
}
socket.nrPendingHeartbeat = true;
try {
socket.ping();
} catch(err) {}
},node.heartbeat);
}
node.emit('opened',{count:'',id:id});
}
});
socket.on('close',function() {
clearInterval(node.heartbeatInterval);
if (node.isServer) {
delete node._clients[id];
node.emit('closed',{count:Object.keys(node._clients).length,id:id});
} else {
node.emit('closed',{count:'',id:id});
}
if (!node.closing && !node.isServer) {
clearTimeout(node.tout);
node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ?
}
});
socket.on('message',function(data,flags) {
node.handleEvent(id,socket,'message',data,flags);
});
socket.nrErrorHandler = function(err) {
clearInterval(node.heartbeatInterval);
node.emit('erro',{err:err,id:id});
if (!node.closing && !node.isServer) {
clearTimeout(node.tout);
node.tout = setTimeout(function() { startconn(); }, 3000); // try to reconnect every 3 secs... bit fast ?
}
}
socket.on('error',socket.nrErrorHandler);
socket.on('ping', function() {
socket.nrPendingHeartbeat = false;
})
socket.on('pong', function() {
socket.nrPendingHeartbeat = false;
})
}
if (node.isServer) {
activeListenerNodes++;
if (!serverUpgradeAdded) {
RED.server.on('upgrade', handleServerUpgrade);
serverUpgradeAdded = true
}
var path = RED.settings.httpNodeRoot || "/";
path = path + (path.slice(-1) == "/" ? "":"/") + (node.path.charAt(0) == "/" ? node.path.substring(1) : node.path);
node.fullPath = path;
if (listenerNodes.hasOwnProperty(path)) {
node.error(RED._("websocket.errors.duplicate-path",{path: node.path}));
return;
}
listenerNodes[node.fullPath] = node;
var serverOptions = {
noServer: true
}
if (RED.settings.webSocketNodeVerifyClient) {
serverOptions.verifyClient = RED.settings.webSocketNodeVerifyClient;
}
// Create a WebSocket Server
node.server = new ws.Server(serverOptions);
node.server.setMaxListeners(0);
node.server.on('connection', handleConnection);
// Not adding server-initiated heartbeats yet
// node.heartbeatInterval = setInterval(function() {
// node.server.clients.forEach(function(ws) {
// if (ws.nrPendingHeartbeat) {
// // No pong received
// ws.terminate();
// ws.nrErrorHandler(new Error("timeout"));
// return;
// }
// ws.nrPendingHeartbeat = true;
// ws.ping();
// });
// })
}
else {
node.closing = false;
startconn(); // start outbound connection
}
node.on("close", function() {
if (node.heartbeatInterval) {
clearInterval(node.heartbeatInterval);
}
if (node.isServer) {
delete listenerNodes[node.fullPath];
node.server.close();
node._inputNodes = [];
activeListenerNodes--;
// if (activeListenerNodes === 0 && serverUpgradeAdded) {
// RED.server.removeListener('upgrade', handleServerUpgrade);
// serverUpgradeAdded = false;
// }
}
else {
node.closing = true;
node.server.close();
if (node.tout) {
clearTimeout(node.tout);
node.tout = null;
}
}
});
}
RED.nodes.registerType("websocket-listener",WebSocketListenerNode);
RED.nodes.registerType("websocket-client",WebSocketListenerNode);
WebSocketListenerNode.prototype.registerInputNode = function(/*Node*/handler) {
this._inputNodes.push(handler);
}
WebSocketListenerNode.prototype.removeInputNode = function(/*Node*/handler) {
this._inputNodes.forEach(function(node, i, inputNodes) {
if (node === handler) {
inputNodes.splice(i, 1);
}
});
}
WebSocketListenerNode.prototype.handleEvent = function(id,/*socket*/socket,/*String*/event,/*Object*/data,/*Object*/flags) {
var msg;
if (this.wholemsg) {
try {
msg = JSON.parse(data);
if (typeof msg !== "object" && !Array.isArray(msg) && (msg !== null)) {
msg = { payload:msg };
}
}
catch(err) {
msg = { payload:data };
}
} else {
msg = {
payload:data
};
}
msg._session = {type:"websocket",id:id};
for (var i = 0; i < this._inputNodes.length; i++) {
this._inputNodes[i].send(msg);
}
}
WebSocketListenerNode.prototype.broadcast = function(data) {
if (this.isServer) {
for (let client in this._clients) {
if (this._clients.hasOwnProperty(client)) {
try {
this._clients[client].send(data);
} catch(err) {
this.warn(RED._("websocket.errors.send-error")+" "+client+" "+err.toString())
}
}
}
}
else {
try {
this.server.send(data);
} catch(err) {
this.warn(RED._("websocket.errors.send-error")+" "+err.toString())
}
}
}
WebSocketListenerNode.prototype.reply = function(id,data) {
var session = this._clients[id];
if (session) {
try {
session.send(data);
}
catch(e) { // swallow any errors
}
}
}
function WebSocketInNode(n) {
RED.nodes.createNode(this,n);
this.server = (n.client)?n.client:n.server;
var node = this;
this.serverConfig = RED.nodes.getNode(this.server);
if (this.serverConfig) {
this.serverConfig.registerInputNode(this);
// TODO: nls
this.serverConfig.on('opened', function(event) {
node.status({
fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count}),
event:"connect",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('erro', function(event) {
node.status({
fill:"red",shape:"ring",text:"common.status.error",
event:"error",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('closed', function(event) {
var status;
if (event.count > 0) {
status = {fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count})};
} else {
status = {fill:"red",shape:"ring",text:"common.status.disconnected"};
}
status.event = "disconnect";
status._session = {type:"websocket",id:event.id}
node.status(status);
});
} else {
this.error(RED._("websocket.errors.missing-conf"));
}
this.on('close', function() {
if (node.serverConfig) {
node.serverConfig.removeInputNode(node);
}
node.status({});
});
}
RED.nodes.registerType("websocket in",WebSocketInNode);
function WebSocketOutNode(n) {
RED.nodes.createNode(this,n);
var node = this;
this.server = (n.client)?n.client:n.server;
this.serverConfig = RED.nodes.getNode(this.server);
if (!this.serverConfig) {
return this.error(RED._("websocket.errors.missing-conf"));
}
else {
// TODO: nls
this.serverConfig.on('opened', function(event) {
node.status({
fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count}),
event:"connect",
_session: {type:"websocket",id:event.id}
});
});
this.serverConfig.on('erro', function(event) {
node.status({
fill:"red",shape:"ring",text:"common.status.error",
event:"error",
_session: {type:"websocket",id:event.id}
})
});
this.serverConfig.on('closed', function(event) {
var status;
if (event.count > 0) {
status = {fill:"green",shape:"dot",text:RED._("websocket.status.connected",{count:event.count})};
} else {
status = {fill:"red",shape:"ring",text:"common.status.disconnected"};
}
status.event = "disconnect";
status._session = {type:"websocket",id:event.id}
node.status(status);
});
}
this.on("input", function(msg, nodeSend, nodeDone) {
var payload;
if (this.serverConfig.wholemsg) {
var sess;
if (msg._session) { sess = JSON.stringify(msg._session); }
delete msg._session;
payload = JSON.stringify(msg);
if (sess) { msg._session = JSON.parse(sess); }
}
else if (msg.hasOwnProperty("payload")) {
if (!Buffer.isBuffer(msg.payload)) { // if it's not a buffer make sure it's a string.
payload = RED.util.ensureString(msg.payload);
}
else {
payload = msg.payload;
}
}
if (payload) {
if (msg._session && msg._session.type == "websocket") {
node.serverConfig.reply(msg._session.id,payload);
} else {
node.serverConfig.broadcast(payload,function(error) {
if (!!error) {
node.warn(RED._("websocket.errors.send-error")+inspect(error));
}
});
}
}
nodeDone();
});
this.on('close', function() {
node.status({});
});
}
RED.nodes.registerType("websocket out",WebSocketOutNode);
}

View File

@ -0,0 +1,156 @@
[
{
"id": "62ea32aa.d73aac",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Example: Link Call Node",
"info": "Link call node can call link in node then get result from link out node.",
"x": 230,
"y": 180,
"wires": []
},
{
"id": "c588bc36.87fec",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓ call link in node",
"info": "",
"x": 440,
"y": 220,
"wires": []
},
{
"id": "cd31efb4d2c6967e",
"type": "link call",
"z": "6312c0588348b2d4",
"name": "",
"links": [
"dbc46892c8d14c37"
],
"timeout": "30",
"x": 420,
"y": 260,
"wires": [
[
"c3db64d1d2260340"
]
]
},
{
"id": "dbc46892c8d14c37",
"type": "link in",
"z": "6312c0588348b2d4",
"name": "",
"links": [],
"x": 315,
"y": 340,
"wires": [
[
"e10575d73f2e5352"
]
]
},
{
"id": "6b61792143b3b0a3",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 240,
"y": 260,
"wires": [
[
"cd31efb4d2c6967e"
]
]
},
{
"id": "e10575d73f2e5352",
"type": "change",
"z": "6312c0588348b2d4",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Hello, World!",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 450,
"y": 340,
"wires": [
[
"cf8438e7137bc0f0"
]
]
},
{
"id": "cf8438e7137bc0f0",
"type": "link out",
"z": "6312c0588348b2d4",
"name": "",
"mode": "return",
"links": [],
"x": 595,
"y": 340,
"wires": []
},
{
"id": "c3db64d1d2260340",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 600,
"y": 260,
"wires": []
},
{
"id": "6d077dfa0987febb",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑called from link call node",
"info": "",
"x": 410,
"y": 380,
"wires": []
},
{
"id": "53b9a0adfd8c4217",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑return to link call node",
"info": "",
"x": 680,
"y": 380,
"wires": []
}
]

View File

@ -0,0 +1,113 @@
[
{
"id": "84222b92.d65d18",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 190,
"y": 180,
"wires": [
[
"b4b9f603.739598"
]
]
},
{
"id": "7b014430.dfd94c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Write string to a file, then read from the file",
"info": "Read file node can read string from a file.",
"x": 220,
"y": 100,
"wires": []
},
{
"id": "b4b9f603.739598",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 380,
"y": 180,
"wires": [
[
"6dc01cac.5c4bf4"
]
]
},
{
"id": "2587adb9.7e60f2",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 770,
"y": 180,
"wires": []
},
{
"id": "6dc01cac.5c4bf4",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 580,
"y": 180,
"wires": [
[
"2587adb9.7e60f2"
]
]
},
{
"id": "f4b4309a.3b78a",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 590,
"y": 220,
"wires": []
},
{
"id": "672d3693.3cabd8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 400,
"y": 140,
"wires": []
}
]

View File

@ -0,0 +1,114 @@
[
{
"id": "8997398f.c5d628",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "😀",
"payloadType": "str",
"x": 170,
"y": 260,
"wires": [
[
"56e32d23.050f44"
]
]
},
{
"id": "4e598e65.1799d",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Read data in specified encoding",
"info": "Read file node can specify encoding of data read from a file.",
"x": 190,
"y": 180,
"wires": []
},
{
"id": "56e32d23.050f44",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 340,
"y": 260,
"wires": [
[
"38fa0579.f2cd8a"
]
]
},
{
"id": "d28c8994.99c0a8",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 260,
"wires": []
},
{
"id": "38fa0579.f2cd8a",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "base64",
"allProps": false,
"x": 540,
"y": 260,
"wires": [
[
"d28c8994.99c0a8"
]
]
},
{
"id": "fa22ca20.ae4528",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read data from file as base64 string",
"info": "",
"x": 600,
"y": 300,
"wires": []
},
{
"id": "148e25ad.98891a",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 360,
"y": 220,
"wires": []
}
]

View File

@ -0,0 +1,132 @@
[
{
"id": "6a0b1d03.d4cee4",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 160,
"y": 220,
"wires": [
[
"d4b00cb7.a5a23"
]
]
},
{
"id": "f17ea1d1.8ecc3",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Read data breaking lines into individual messages",
"info": "Read file node can break read text into messages with individual lines",
"x": 230,
"y": 140,
"wires": []
},
{
"id": "99ae7806.1d6428",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 480,
"y": 220,
"wires": [
[
"70d7892f.d27db8"
]
]
},
{
"id": "7ed8282c.92b338",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 750,
"y": 280,
"wires": []
},
{
"id": "70d7892f.d27db8",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "lines",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 560,
"y": 280,
"wires": [
[
"7ed8282c.92b338"
]
]
},
{
"id": "c1b7e05.1d94b2",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read data from file breaking lines into messages",
"info": "",
"x": 660,
"y": 320,
"wires": []
},
{
"id": "a5f647b2.cf27a8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 500,
"y": 180,
"wires": []
},
{
"id": "d4b00cb7.a5a23",
"type": "template",
"z": "6312c0588348b2d4",
"name": "data",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "plain",
"template": "one\ntwo\nthree!",
"output": "str",
"x": 310,
"y": 220,
"wires": [
[
"99ae7806.1d6428"
]
]
}
]

View File

@ -2,7 +2,7 @@
{ {
"id": "bdd57acc.2edc48", "id": "bdd57acc.2edc48",
"type": "inject", "type": "inject",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "", "name": "",
"props": [ "props": [
{ {
@ -20,8 +20,8 @@
"topic": "", "topic": "",
"payload": "", "payload": "",
"payloadType": "date", "payloadType": "date",
"x": 220, "x": 180,
"y": 1040, "y": 220,
"wires": [ "wires": [
[ [
"7a069b01.0c2324" "7a069b01.0c2324"
@ -31,25 +31,25 @@
{ {
"id": "1fd12220.33953e", "id": "1fd12220.33953e",
"type": "comment", "type": "comment",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "Creating a message stream from lines of data", "name": "Creating a message stream from lines of data",
"info": "File-in node can break read text into messages with individual lines. The messages creates a stream of messages.", "info": "Read file node can break read text into messages with individual lines. The messages creates a stream of messages.",
"x": 270, "x": 230,
"y": 960, "y": 140,
"wires": [] "wires": []
}, },
{ {
"id": "ab6eb213.2a08d", "id": "ab6eb213.2a08d",
"type": "file", "type": "file",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "", "name": "",
"filename": "/tmp/hello.txt", "filename": "/tmp/hello.txt",
"appendNewline": true, "appendNewline": true,
"createDir": false, "createDir": false,
"overwriteFile": "true", "overwriteFile": "true",
"encoding": "none", "encoding": "none",
"x": 540, "x": 500,
"y": 1040, "y": 220,
"wires": [ "wires": [
[ [
"b7ed49b0.649fb8" "b7ed49b0.649fb8"
@ -59,7 +59,7 @@
{ {
"id": "c48d8ae0.9ff3a8", "id": "c48d8ae0.9ff3a8",
"type": "debug", "type": "debug",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "", "name": "",
"active": true, "active": true,
"tosidebar": true, "tosidebar": true,
@ -68,22 +68,22 @@
"complete": "false", "complete": "false",
"statusVal": "", "statusVal": "",
"statusType": "auto", "statusType": "auto",
"x": 810, "x": 770,
"y": 1140, "y": 320,
"wires": [] "wires": []
}, },
{ {
"id": "b7ed49b0.649fb8", "id": "b7ed49b0.649fb8",
"type": "file in", "type": "file in",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "", "name": "",
"filename": "/tmp/hello.txt", "filename": "/tmp/hello.txt",
"format": "lines", "format": "lines",
"chunk": false, "chunk": false,
"sendError": false, "sendError": false,
"encoding": "none", "encoding": "none",
"x": 280, "x": 240,
"y": 1140, "y": 320,
"wires": [ "wires": [
[ [
"83073ebe.fcce4" "83073ebe.fcce4"
@ -93,27 +93,27 @@
{ {
"id": "3c33e69f.6a04ba", "id": "3c33e69f.6a04ba",
"type": "comment", "type": "comment",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "↑read data from file breaking lines into messages", "name": "↑read data from file breaking lines into messages",
"info": "", "info": "",
"x": 380, "x": 340,
"y": 1180, "y": 360,
"wires": [] "wires": []
}, },
{ {
"id": "3598bf7d.5712a", "id": "3598bf7d.5712a",
"type": "comment", "type": "comment",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt", "name": "↓write to /tmp/hello.txt",
"info": "", "info": "",
"x": 560, "x": 520,
"y": 1000, "y": 180,
"wires": [] "wires": []
}, },
{ {
"id": "7a069b01.0c2324", "id": "7a069b01.0c2324",
"type": "template", "type": "template",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "data", "name": "data",
"field": "payload", "field": "payload",
"fieldType": "msg", "fieldType": "msg",
@ -121,8 +121,8 @@
"syntax": "plain", "syntax": "plain",
"template": "Apple\nBanana\nGrape\nOrange", "template": "Apple\nBanana\nGrape\nOrange",
"output": "str", "output": "str",
"x": 370, "x": 330,
"y": 1040, "y": 220,
"wires": [ "wires": [
[ [
"ab6eb213.2a08d" "ab6eb213.2a08d"
@ -132,7 +132,7 @@
{ {
"id": "8d4ed1d0.821fe", "id": "8d4ed1d0.821fe",
"type": "join", "type": "join",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "", "name": "",
"mode": "auto", "mode": "auto",
"build": "string", "build": "string",
@ -145,8 +145,8 @@
"timeout": "", "timeout": "",
"count": "", "count": "",
"reduceRight": false, "reduceRight": false,
"x": 630, "x": 590,
"y": 1140, "y": 320,
"wires": [ "wires": [
[ [
"c48d8ae0.9ff3a8" "c48d8ae0.9ff3a8"
@ -156,7 +156,7 @@
{ {
"id": "83073ebe.fcce4", "id": "83073ebe.fcce4",
"type": "switch", "type": "switch",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "< D", "name": "< D",
"property": "payload", "property": "payload",
"propertyType": "msg", "propertyType": "msg",
@ -170,8 +170,8 @@
"checkall": "true", "checkall": "true",
"repair": true, "repair": true,
"outputs": 1, "outputs": 1,
"x": 470, "x": 430,
"y": 1140, "y": 320,
"wires": [ "wires": [
[ [
"8d4ed1d0.821fe" "8d4ed1d0.821fe"
@ -181,21 +181,21 @@
{ {
"id": "2088e195.f7aebe", "id": "2088e195.f7aebe",
"type": "comment", "type": "comment",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "↓filter data before \"D\"", "name": "↓filter data before \"D\"",
"info": "", "info": "",
"x": 520, "x": 480,
"y": 1100, "y": 280,
"wires": [] "wires": []
}, },
{ {
"id": "b848cdc7.61e06", "id": "b848cdc7.61e06",
"type": "comment", "type": "comment",
"z": "194a3e4f.a92772", "z": "6312c0588348b2d4",
"name": "↑join to single string", "name": "↑join to single string",
"info": "", "info": "",
"x": 670, "x": 630,
"y": 1180, "y": 360,
"wires": [] "wires": []
} }
] ]

View File

@ -0,0 +1,113 @@
[
{
"id": "84222b92.d65d18",
"type": "inject",
"z": "5132b95f037524f9",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 150,
"y": 220,
"wires": [
[
"b4b9f603.739598"
]
]
},
{
"id": "7b014430.dfd94c",
"type": "comment",
"z": "5132b95f037524f9",
"name": "Write string to a file, then read from the file",
"info": "Write file node can write string from a file.",
"x": 180,
"y": 140,
"wires": []
},
{
"id": "b4b9f603.739598",
"type": "file",
"z": "5132b95f037524f9",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 340,
"y": 220,
"wires": [
[
"6dc01cac.5c4bf4"
]
]
},
{
"id": "2587adb9.7e60f2",
"type": "debug",
"z": "5132b95f037524f9",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 220,
"wires": []
},
{
"id": "6dc01cac.5c4bf4",
"type": "file in",
"z": "5132b95f037524f9",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 540,
"y": 220,
"wires": [
[
"2587adb9.7e60f2"
]
]
},
{
"id": "f4b4309a.3b78a",
"type": "comment",
"z": "5132b95f037524f9",
"name": "↑read result from file",
"info": "",
"x": 550,
"y": 260,
"wires": []
},
{
"id": "672d3693.3cabd8",
"type": "comment",
"z": "5132b95f037524f9",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 360,
"y": 180,
"wires": []
}
]

View File

@ -0,0 +1,118 @@
[
{
"id": "704479e1.399388",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "filename",
"v": "/tmp/hello.txt",
"vt": "str"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 190,
"y": 260,
"wires": [
[
"402f3b7e.988014"
]
]
},
{
"id": "8e876a75.e9beb8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Write string to a file specied by filename property, the read from the file",
"info": "Write file node can target file using `filename` property.",
"x": 310,
"y": 180,
"wires": []
},
{
"id": "402f3b7e.988014",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 350,
"y": 260,
"wires": [
[
"26e077d6.bbcd98"
]
]
},
{
"id": "97b6b6b2.a54b38",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 260,
"wires": []
},
{
"id": "26e077d6.bbcd98",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 540,
"y": 260,
"wires": [
[
"97b6b6b2.a54b38"
]
]
},
{
"id": "85062297.da79",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 550,
"y": 300,
"wires": []
},
{
"id": "7316c4fc.b1dcdc",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to file specified by filename property",
"info": "",
"x": 460,
"y": 220,
"wires": []
}
]

View File

@ -0,0 +1,85 @@
[
{
"id": "4ac00fb0.d5f52",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 180,
"y": 220,
"wires": [
[
"542cc2f4.92857c"
]
]
},
{
"id": "671f8295.0e6f6c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Delete a file",
"info": "Write file node can delete a file.",
"x": 130,
"y": 160,
"wires": []
},
{
"id": "542cc2f4.92857c",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "delete",
"encoding": "none",
"x": 380,
"y": 220,
"wires": [
[
"a24da523.5babe8"
]
]
},
{
"id": "a24da523.5babe8",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 220,
"wires": []
},
{
"id": "51157051.2f62",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓delete a file",
"info": "",
"x": 350,
"y": 180,
"wires": []
}
]

View File

@ -0,0 +1,113 @@
[
{
"id": "e4ef1f5e.7cd82",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "Base64 encoded string",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "8J+YgA==",
"payloadType": "str",
"x": 200,
"y": 220,
"wires": [
[
"72b37cc8.177054"
]
]
},
{
"id": "f5997af4.5a9298",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Specify encoding of written data",
"info": "Write file node can specify encoding of data.",
"x": 170,
"y": 140,
"wires": []
},
{
"id": "72b37cc8.177054",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "base64",
"x": 420,
"y": 220,
"wires": [
[
"2da33ec.f45cac2"
]
]
},
{
"id": "2e814354.278c8c",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 220,
"wires": []
},
{
"id": "2da33ec.f45cac2",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 620,
"y": 220,
"wires": [
[
"2e814354.278c8c"
]
]
},
{
"id": "ec754c99.84bfd",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write string with base64 encoding",
"info": "",
"x": 480,
"y": 180,
"wires": []
},
{
"id": "3e6704ff.4ce25c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 630,
"y": 260,
"wires": []
}
]

View File

@ -21,7 +21,7 @@
<dt>msg <span class="property-type">object</span></dt> <dt>msg <span class="property-type">object</span></dt>
<dd>A msg object containing information to populate the template.</dd> <dd>A msg object containing information to populate the template.</dd>
<dt class="optional">template <span class="property-type">string</span></dt> <dt class="optional">template <span class="property-type">string</span></dt>
<dd>A template to be populated from msg.payload. If not configured in the edit panel, <dd>A template to be populated from <code>msg.payload</code>. If not configured in the edit panel,
this can be set as a property of msg.</dd> this can be set as a property of msg.</dd>
</dl> </dl>
<h3>Outputs</h3> <h3>Outputs</h3>

View File

@ -60,5 +60,5 @@
for the next topic. for the next topic.
</p> </p>
<p><b>Note</b>: In rate limit mode the maximum queue depth can be set by a property in your <p><b>Note</b>: In rate limit mode the maximum queue depth can be set by a property in your
<i>settings.js</i> file. For example <code>nodeMessageBufferMaxLength: 1000,</code> <i>settings.js</i> file. For example <code>nodeMessageBufferMaxLength: 1000,</code></p>
</script> </script>

View File

@ -25,7 +25,7 @@
<h3>Details</h3> <h3>Details</h3>
<p>In RBE mode this node will block until the <code>msg.payload</code>, <p>In RBE mode this node will block until the <code>msg.payload</code>,
(or selected property) value is different to the previous one. (or selected property) value is different to the previous one.
If required it can ignore the intial value, so as not to send anything at start.</p> If required it can ignore the initial value, so as not to send anything at start.</p>
<p>The <a href="https://en.wikipedia.org/wiki/Deadband" target="_blank">Deadband</a> modes will block the incoming value <p>The <a href="https://en.wikipedia.org/wiki/Deadband" target="_blank">Deadband</a> modes will block the incoming value
<i>unless</i> its change is greater or greater-equal than &plusmn; the band gap away from a previous value.</p> <i>unless</i> its change is greater or greater-equal than &plusmn; the band gap away from a previous value.</p>
<p>The Narrowband modes will block the incoming value, <p>The Narrowband modes will block the incoming value,
@ -37,5 +37,5 @@
ignoring any values out of range, or the previous input value, which resets the set point, thus allowing ignoring any values out of range, or the previous input value, which resets the set point, thus allowing
gradual drift (deadband), or a step change (narrowband).</p> gradual drift (deadband), or a step change (narrowband).</p>
<p><b>Note:</b> This works on a per <code>msg.topic</code> basis, though this can be changed to another property if desired. <p><b>Note:</b> This works on a per <code>msg.topic</code> basis, though this can be changed to another property if desired.
This means that a single rbe node can handle multiple different topics at the same time.</p> This means that a single filter node can handle multiple different topics at the same time.</p>
</script> </script>

View File

@ -52,7 +52,7 @@
<dt class="optional">topic <span class="property-type">string|object|array</span></dt> <dt class="optional">topic <span class="property-type">string|object|array</span></dt>
<dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property <dd>For the <code>"subscribe"</code> and <code>"unsubscribe"</code> actions, this property
provides the topic. It can be set as either:<ul> provides the topic. It can be set as either:<ul>
<li>a String continaing the topic filter</li> <li>a String containing the topic filter</li>
<li>an Object containing <code>topic</code> and <code>qos</code> properties</li> <li>an Object containing <code>topic</code> and <code>qos</code> properties</li>
<li>an array of either strings or objects to handle multiple topics in one</li> <li>an array of either strings or objects to handle multiple topics in one</li>
</ul> </ul>

View File

@ -52,7 +52,7 @@
<dd>In case any redirects occurred while processing the request, this property is the final redirected url. <dd>In case any redirects occurred while processing the request, this property is the final redirected url.
Otherwise, the url of the original request.</dd> Otherwise, the url of the original request.</dd>
<dt>responseCookies <span class="property-type">object</span></dt> <dt>responseCookies <span class="property-type">object</span></dt>
<dd>If the response includes cookies, this propery is an object of name/value pairs for each cookie.</dd> <dd>If the response includes cookies, this property is an object of name/value pairs for each cookie.</dd>
<dt>redirectList <span class="property-type">array</span></dt> <dt>redirectList <span class="property-type">array</span></dt>
<dd>If the request was redirected one or more times, the accumulated information will be added to this property. `location` is the next redirect destination. `cookies` is the cookies returned from the redirect source.</dd> <dd>If the request was redirected one or more times, the accumulated information will be added to this property. `location` is the next redirect destination. `cookies` is the cookies returned from the redirect source.</dd>
</dl> </dl>

View File

@ -60,7 +60,7 @@
</p> </p>
<p>When operating in this mode, the node will not set the <code>msg.parts.count</code> <p>When operating in this mode, the node will not set the <code>msg.parts.count</code>
property as it does not know how many messages to expect in the stream. This property as it does not know how many messages to expect in the stream. This
means it cannot be used with the <b>join</b> node in its automatic mode</p> means it cannot be used with the <b>join</b> node in its automatic mode.</p>
</script> </script>
<script type="text/html" data-help-name="join"> <script type="text/html" data-help-name="join">

View File

@ -46,4 +46,6 @@
<code>{{global[store].名前}}</code>を用います。 <code>{{global[store].名前}}</code>を用います。
</p> </p>
<p><b>注: </b>デフォルトでは、<i>mustache</i>形式は置換対象のHTML要素をエスケープします。これを抑止するには<code>{{{三重}}}</code>括弧形式を使います。</p> <p><b>注: </b>デフォルトでは、<i>mustache</i>形式は置換対象のHTML要素をエスケープします。これを抑止するには<code>{{{三重}}}</code>括弧形式を使います。</p>
<p>もし、コンテンツの中で<code>{{ }}</code>を出力する必要がある場合は、テンプレートで使われる記号文字を変えることもできます。例えば、<code>[[ ]]</code>を代わりに用いるには、テンプレートの先頭に以下の行を追加します。</p>
<pre>{{=[[ ]]=}}</pre>
</script> </script>

View File

@ -25,11 +25,14 @@
<dt class="optional">reset</dt> <dt class="optional">reset</dt>
<dd>受信メッセージでこのプロパティを任意の値に設定すると、ノードが保持する全ての未送信メッセージをクリアします。</dd> <dd>受信メッセージでこのプロパティを任意の値に設定すると、ノードが保持する全ての未送信メッセージをクリアします。</dd>
<dt class="optional">flush</dt> <dt class="optional">flush</dt>
<dd>受信メッセージでこのプロパティを任意の値に設定すると、ノードが保持する全ての未送信メッセージを直ちに送信します。</dd> <dd>本プロパティに数値が設定されたメッセージを受信すると、直ちに指定された数のメッセージを送信します。もし他の型(例えば真偽型)が設定されている場合は、ノードが保持している全ての未送信メッセージを直ちに送信します。</dd>
<dt class="optional">toFront</dt>
<dd>流量制御モードにおいて、本プロパティに真偽型<code>true</code>が設定されたメッセージを受け取ると、キューの先頭に追加され、その後に送信されます。<code>msg.flush=1</code>と組み合わせて用いると、すぐに再送信できます。</dd>
</dl> </dl>
<h3>詳細</h3> <h3>詳細</h3>
<p>メッセージを遅延させるように設定する場合、遅延時間は固定値、範囲内の乱数値、メッセージ毎の動的な指定値のいずれかを指定できます。</p> <p>メッセージを遅延させるように設定する場合、遅延時間は固定値、範囲内の乱数値、メッセージ毎の動的な指定値のいずれかを指定できます。各メッセージは、到着時刻に基づいて、他のメッセージとは独立して遅延されます。</p>
<p>流量制御する場合、メッセージは指定した時間間隔内に分散して送信します。キューに残っているメッセージ数はノードのステータスに表示されます。受け取った中間メッセージを破棄することも可能です。</p> <p>流量制御する場合、メッセージは指定した時間間隔内に分散して送信します。キューに残っているメッセージ数はノードのステータスに表示されます。受け取った中間メッセージを破棄することも可能です。</p>
<p>流量値を上書きできるように設定されている場合、新しい流量値はすぐに適用されます。この流量値は、再度変更されるまで、本ノードがリセットされるまで、またはフローが再実行されるまで有効です。</p> <p>流量値を上書きできるように設定されている場合、新しい流量値はすぐに適用されます。この流量値は、再度変更されるまで、本ノードがリセットされるまで、またはフローが再実行されるまで有効です。</p>
<p>流量制御は全てのメッセージに適用することも、<code>msg.topic</code>値でグループ化して適用することも可能です。グループ化すると、中間メッセージは自動的に破棄されます。時間間隔毎に全てのトピックの最新メッセージを送信するか、次のトピックの最新メッセージを送信するかを指定できます。</p> <p>流量制御は全てのメッセージに適用することも、<code>msg.topic</code>値でグループ化して適用することも可能です。グループ化すると、中間メッセージは自動的に破棄されます。時間間隔毎に全てのトピックの最新メッセージを送信するか、次のトピックの最新メッセージを送信するかを指定できます。</p>
<p><b></b>: 流量制御モードでは、キューの大きさの最大値を<i>settings.js</i>ファイルのプロパティに設定できます。例えば、次の様な設定です。<code>nodeMessageBufferMaxLength: 1000,</code></p>
</script> </script>

View File

@ -27,5 +27,5 @@
<p>不感帯モードでは%による指定もサポートしています。入力と前の値の差分がX%より大きな場合に出力を行います。</p> <p>不感帯モードでは%による指定もサポートしています。入力と前の値の差分がX%より大きな場合に出力を行います。</p>
<p>狭帯域(narrowband)モードでは、前の値に対する差分が一定値より大きな場合に入力ペイロードをブロックします。このモードは、故障したセンサから発生する外れ値を無視する時などに有用です。</p> <p>狭帯域(narrowband)モードでは、前の値に対する差分が一定値より大きな場合に入力ペイロードをブロックします。このモードは、故障したセンサから発生する外れ値を無視する時などに有用です。</p>
<p>不感帯モードと狭帯域モードでは、以前の有効出力値、もしくは、以前の入力値との比較ができます。有効出力値を用いると範囲外の値を無視することが、入力値を用いると設定点がリセットされるため漸次的変化(不感帯モード)もしくは段階的変化(狭帯域モード)が可能です。</p> <p>不感帯モードと狭帯域モードでは、以前の有効出力値、もしくは、以前の入力値との比較ができます。有効出力値を用いると範囲外の値を無視することが、入力値を用いると設定点がリセットされるため漸次的変化(不感帯モード)もしくは段階的変化(狭帯域モード)が可能です。</p>
<p><b>注:</b> このノードは<code>msg.topic</code>毎に動作します。そのため、ひとつのrbeノードで複数の異なるトピックを同時に扱うことができます。</p> <p><b>注:</b> このノードは<code>msg.topic</code>毎に動作します。そのため、ひとつのfilterードで複数の異なるトピックを同時に扱うことができます。</p>
</script> </script>

View File

@ -26,11 +26,46 @@
<dd>0: 最大1度到着, 1: 一度以上到着, 2: 1度のみ到着</dd> <dd>0: 最大1度到着, 1: 一度以上到着, 2: 1度のみ到着</dd>
<dt>retain <span class="property-type">真偽値</span></dt> <dt>retain <span class="property-type">真偽値</span></dt>
<dd>真の場合、メッセージを保持。メッセージが古い値の場合があります。</dd> <dd>真の場合、メッセージを保持。メッセージが古い値の場合があります。</dd>
<dt class="optional">responseTopic <span class="property-type">文字列</span></dt>
<dd><b>MQTTv5</b>: メッセージのMQTT応答トピック</dd>
<dt class="optional">correlationData <span class="property-type">バッファ</span></dt>
<dd><b>MQTTv5</b>: メッセージの相関データ</dd>
<dt class="optional">contentType <span class="property-type">文字列</span></dt>
<dd><b>MQTTv5</b>: ペイロードのコンテントタイプ</dd>
<dt class="optional">userProperties <span class="property-type">オブジェクト</span></dt>
<dd><b>MQTTv5</b>: メッセージのユーザプロパティ</dd>
<dt class="optional">messageExpiryInterval <span class="property-type">数値</span></dt>
<dd><b>MQTTv5</b>: 秒単位のメッセージの有効期限</dd>
</dl> </dl>
<h3>詳細</h3> <h3>詳細</h3>
<p>購読トピックにはMQTTのワイルドカード(+: 1レベル, #: 複数レベル)を含めることができます。</p> <p>購読トピックにはMQTTのワイルドカード(+: 1レベル, #: 複数レベル)を含めることができます。</p>
<p>このードの利用のためには、MQTTブローカへの接続設定が必要です。この設定は鉛筆アイコンをクリックすることで行えます。</p> <p>このードの利用のためには、MQTTブローカへの接続設定が必要です。この設定は鉛筆アイコンをクリックすることで行えます。</p>
<p>MQTT(inおよびout)ノードはブローカへの接続設定を必要に応じて共有できます。</p> <p>MQTT(inおよびout)ノードはブローカへの接続設定を必要に応じて共有できます。</p>
<h4>動的購読</h4>
ードは、MQTTの接続と購読を動的に制御するよう設定できます。有効にすると、本ードの入力にメッセージを渡すことで制御できます。
<h3>入力</h3>
<p>これらは、動的購読が設定されている場合のみ適用されます。</p>
<dl class="message-properties">
<dt>action <span class="property-type">文字列</span></dt>
<dd>本ノードが行う動作の名前。利用可能な動作は<code>"connect"</code><code>"disconnect"</code><code>"subscribe"</code><code>"unsubscribe"</code>です。</dd>
<dt class="optional">topic <span class="property-type">文字列|オブジェクト|配列</span></dt>
<dd><code>"subscribe"</code><code>"unsubscribe"</code>の動作に対して、本プロパティはトピックを提供します。次のいずれかを設定できます:<ul>
<li>トピックフィルターを含む文字列</li>
<li><code>topic</code><code>qos</code>プロパティを持つオブジェクト</li>
<li>複数のトピックを扱う文字列やオブジェクトの配列</li>
</ul>
</dd>
<dt class="optional">broker <span class="property-type">broker</span> </dt>
<dd><code>"connect"</code>の動作に対して、本プロパティは次の様な個々のブローカ設定を上書きします: <ul>
<li><code>broker</code></li>
<li><code>port</code></li>
<li><code>url</code> - 完全な接続URLを提供するために、brokerとportを上書き</li>
<li><code>username</code></li>
<li><code>password</code></li>
</ul>
<p>本プロパティが設定され既にブローカが接続されている場合、<code>force</code>プロパティを設定しない限り、エラーがログに記録されます。設定された場合はブローカから切断され、新しい設定を適用して再接続します。</p>
</dd>
</dl>
</script> </script>
<script type="text/html" data-help-name="mqtt out"> <script type="text/html" data-help-name="mqtt out">
@ -39,15 +74,24 @@
<dl class="message-properties"> <dl class="message-properties">
<dt>payload <span class="property-type">文字列 | バッファ</span></dt> <dt>payload <span class="property-type">文字列 | バッファ</span></dt>
<dd>発行するペイロード。プロパティが設定されていない場合には、メッセージは送信されません。空のメッセージを送信するには、プロパティに空文字列を設定します。</dd> <dd>発行するペイロード。プロパティが設定されていない場合には、メッセージは送信されません。空のメッセージを送信するには、プロパティに空文字列を設定します。</dd>
<dt class="optional">topic <span class="property-type">文字列</span></dt> <dt class="optional">topic <span class="property-type">文字列</span></dt>
<dd>発行対象のMQTTトピック</dd> <dd>発行対象のMQTTトピック</dd>
<dt class="optional">qos <span class="property-type">数値</span></dt> <dt class="optional">qos <span class="property-type">数値</span></dt>
<dd>0: 最大一度到着, 1: 一度以上到着, 2: 一度のみ到着。デフォルトは0です。</dd> <dd>0: 最大一度到着, 1: 一度以上到着, 2: 一度のみ到着。デフォルトは0です。</dd>
<dt class="optional">retain <span class="property-type">真偽値</span></dt> <dt class="optional">retain <span class="property-type">真偽値</span></dt>
<dd>真の場合、メッセージをブローカに保持します。デフォルトは偽です。</dd> <dd>真の場合、メッセージをブローカに保持します。デフォルトは偽です。</dd>
<dt class="optional">responseTopic <span class="property-type">文字列</span></dt>
<dd><b>MQTTv5</b>: メッセージのMQTT応答トピック</dd>
<dt class="optional">correlationData <span class="property-type">バッファ</span></dt>
<dd><b>MQTTv5</b>: メッセージの相関データ</dd>
<dt class="optional">contentType <span class="property-type">文字列</span></dt>
<dd><b>MQTTv5</b>: ペイロードのコンテントタイプ</dd>
<dt class="optional">userProperties <span class="property-type">オブジェクト</span></dt>
<dd><b>MQTTv5</b>: メッセージのユーザプロパティ</dd>
<dt class="optional">messageExpiryInterval <span class="property-type">数値</span></dt>
<dd><b>MQTTv5</b>: 秒単位のメッセージの有効期限</dd>
<dt class="optional">topicAlias <span class="property-type">数値</span></dt>
<dd><b>MQTTv5</b>: 使用するMQTTトピックエイリアス</dd>
</dl> </dl>
<h3>詳細</h3> <h3>詳細</h3>
<p><code>msg.payload</code>を発行するメッセージのペイロードとして用います。ペイロードがオブジェクトの場合、送信の際にJSON文字列に変換します。ペイロードがバイナリバッファの場合、そのまま送信します。</p> <p><code>msg.payload</code>を発行するメッセージのペイロードとして用います。ペイロードがオブジェクトの場合、送信の際にJSON文字列に変換します。ペイロードがバイナリバッファの場合、そのまま送信します。</p>
@ -55,6 +99,24 @@
<p>同様に、QoSとretainもードの設定、もしくは、ードの設定が空の場合には、それぞれ<code>msg.qos</code>および<code>msg.retain</code>で指定できます。以前ブローカに保存したトピックをクリアするには、retainフラグを設定して当該トピックに空のメッセージを発行します。</p> <p>同様に、QoSとretainもードの設定、もしくは、ードの設定が空の場合には、それぞれ<code>msg.qos</code>および<code>msg.retain</code>で指定できます。以前ブローカに保存したトピックをクリアするには、retainフラグを設定して当該トピックに空のメッセージを発行します。</p>
<p>このードの利用のためには、MQTTブローカへの接続設定が必要です。この設定は鉛筆アイコンをクリックすることで行えます。</p> <p>このードの利用のためには、MQTTブローカへの接続設定が必要です。この設定は鉛筆アイコンをクリックすることで行えます。</p>
<p>MQTT(inおよびout)ノードはブローカへの接続設定を必要に応じて共有できます。</p> <p>MQTT(inおよびout)ノードはブローカへの接続設定を必要に応じて共有できます。</p>
<h4>動的制御</h4>
本ノードによって接続を動的に制御できます。本ノードが以下の制御メッセージのいずれかを受け取った際は、ペイロードと同じ様にパブリッシュされることはありません。
<h3>入力</h3>
<dl class="message-properties">
<dt>action <span class="property-type">文字列</span></dt>
<dd>本ノードが行う動作の名前。利用可能な動作は<code>"connect"</code><code>"disconnect"</code><code>"subscribe"</code><code>"unsubscribe"</code>です。</dd>
<dt class="optional">broker <span class="property-type">broker</span> </dt>
<dd><code>"connect"</code>の動作に対して、本プロパティは次の様な個々のブローカ設定を上書きします: <ul>
<li><code>broker</code></li>
<li><code>port</code></li>
<li><code>url</code> - 完全な接続URLを提供するために、brokerとportを上書き</li>
<li><code>username</code></li>
<li><code>password</code></li>
</ul>
<p>本プロパティが設定され既にブローカが接続されている場合、<code>force</code>プロパティを設定しない限り、エラーがログに記録されます。設定された場合はブローカから切断され、新しい設定を適用して再接続します。</p>
</dd>
</dl>
</script> </script>
<script type="text/html" data-help-name="mqtt-broker"> <script type="text/html" data-help-name="mqtt-broker">
@ -70,5 +132,4 @@
<h4>WebSocket</h4> <h4>WebSocket</h4>
<p>WebSocketによる接続を行うように設定できます。WebSocketを利用するには、サーバフィールドに接続先のURIを完全な形式で記述します。以下に例を示します。</p> <p>WebSocketによる接続を行うように設定できます。WebSocketを利用するには、サーバフィールドに接続先のURIを完全な形式で記述します。以下に例を示します。</p>
<pre>ws://example.com:4000/mqtt</pre> <pre>ws://example.com:4000/mqtt</pre>
</script> </script>

View File

@ -36,7 +36,7 @@
<h3>詳細</h3> <h3>詳細</h3>
<p>「列名」にカラム名のリストを指定することができます。CSVからオブジェクトに変換を行う際、カラム名をプロパティ名として使用します。「列名」の代わりに、CSVデータの1行目にカラム名を含めることもできます。</p> <p>「列名」にカラム名のリストを指定することができます。CSVからオブジェクトに変換を行う際、カラム名をプロパティ名として使用します。「列名」の代わりに、CSVデータの1行目にカラム名を含めることもできます。</p>
<p>CSVへの変換を行う際には、オブジェクトから取り出すべきプロパティとその順序を「列名」を参照して決めます。</p> <p>CSVへの変換を行う際には、オブジェクトから取り出すべきプロパティとその順序を「列名」を参照して決めます。</p>
<p>列名がない場合、本ノードは<code>msg.columns</code>プロパティの単純なコンマ区切りリストを使用して、何を抽出するかを決定します。もしそれが存在しない場合、すべてのオブジェクトプロパティを見つけた順序で出力します。</p> <p>列名がない場合、本ノードは<code>msg.columns</code>プロパティの単純なコンマ区切りリストを使用して、何をどの順序で抽出するかを決定します。もし存在しない場合、すべてのオブジェクトプロパティを見つけた順序で出力します。</p>
<p>入力が配列の場合には、「列名」はカラム名を表す行の出力指定がされた場合だけ用います。</p> <p>入力が配列の場合には、「列名」はカラム名を表す行の出力指定がされた場合だけ用います。</p>
<p>「数値を変換する」オプションがチェックされている場合、文字列型の数値が数値として返されます。つまり「1,"1.5",2」の真ん中の値が数値になります。</p> <p>「数値を変換する」オプションがチェックされている場合、文字列型の数値が数値として返されます。つまり「1,"1.5",2」の真ん中の値が数値になります。</p>
<p>「空の文字を含む」オプションがチェックされている場合、空の文字列が結果に返されます。つまり「"1","",3」の真ん中の値が空の文字列になります。</p> <p>「空の文字を含む」オプションがチェックされている場合、空の文字列が結果に返されます。つまり「"1","",3」の真ん中の値が空の文字列になります。</p>

View File

@ -52,7 +52,6 @@
<p>このモードで処理する際には、メッセージ数を予め知ることができないため、<code>msg.parts.count</code>プロパティは設定されません。従って、<b>join</b>ノードの「自動モード」と組み合わせることはできません。</p> <p>このモードで処理する際には、メッセージ数を予め知ることができないため、<code>msg.parts.count</code>プロパティは設定されません。従って、<b>join</b>ノードの「自動モード」と組み合わせることはできません。</p>
</script> </script>
<script type="text/html" data-help-name="join"> <script type="text/html" data-help-name="join">
<p>メッセージ列を結合して一つのメッセージにします。</p> <p>メッセージ列を結合して一つのメッセージにします。</p>
<p>メッセージの結合には次の3つのモードが利用できます。</p> <p>メッセージの結合には次の3つのモードが利用できます。</p>
@ -80,6 +79,10 @@
</dd> </dd>
<dt class="optional">complete</dt> <dt class="optional">complete</dt>
<dd>設定されている場合、本ードはペイロードを追加し、保持しているメッセージを送信します。ペイロードを追加したくない場合は、msgから削除してください。</dd> <dd>設定されている場合、本ードはペイロードを追加し、保持しているメッセージを送信します。ペイロードを追加したくない場合は、msgから削除してください。</dd>
<dt class="optional">reset</dt>
<dd>設定されている場合、本ノードは部分的に完成したメッセージを送信せず、削除します。</dd>
<dt class="optional">restartTimeout</dt>
<dd>設定されている場合、本ノードにタイムアウトが設定され、そのタイムアウトを用いて処理が再開されます。</dd>
</dl> </dl>
<h3>詳細</h3> <h3>詳細</h3>
@ -96,7 +99,7 @@
</ul> </ul>
<p>出力メッセージのその他のプロパティはメッセージを送信する直前のメッセージをコピーします。</p> <p>出力メッセージのその他のプロパティはメッセージを送信する直前のメッセージをコピーします。</p>
<p><i>合計値</i>」には出力メッセージを送信する前に受信すべきメッセージ数を指定します。オブジェクト出力の場合、この合計値に達すると後続メッセージの到着毎にメッセージを出力するように設定することもできます。</p> <p><i>合計値</i>」には出力メッセージを送信する前に受信すべきメッセージ数を指定します。オブジェクト出力の場合、この合計値に達すると後続メッセージの到着毎にメッセージを出力するように設定することもできます。</p>
<p><i></i>」には新規メッセージを送信するまでの経過時間を設定します。</p> <p><i></i>」には新規メッセージを送信するまでの経過時間を設定します。<code>msg.restartTimeout</code>プロパティを設定したメッセージを渡すことで、指定した時間で再開できます。</p>
<p><code>msg.complete</code>プロパティを設定したメッセージを受信すると、出力メッセージを送信します。この時、メッセージ列の数をリセットします。</p> <p><code>msg.complete</code>プロパティを設定したメッセージを受信すると、出力メッセージを送信します。この時、メッセージ列の数をリセットします。</p>
<p><code>msg.reset</code>プロパティを設定したメッセージを受信すると、部分的に受信済みのメッセージを破棄します。これらのメッセージは送信されません。この時、メッセージ列の数をリセットします。</p> <p><code>msg.reset</code>プロパティを設定したメッセージを受信すると、部分的に受信済みのメッセージを破棄します。これらのメッセージは送信されません。この時、メッセージ列の数をリセットします。</p>

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/nodes", "name": "@node-red/nodes",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,10 +15,10 @@
} }
], ],
"dependencies": { "dependencies": {
"acorn": "8.6.0", "acorn": "8.7.0",
"acorn-walk": "8.2.0", "acorn-walk": "8.2.0",
"ajv": "8.8.2", "ajv": "8.8.2",
"body-parser": "1.19.0", "body-parser": "1.19.1",
"cheerio": "1.0.0-rc.10", "cheerio": "1.0.0-rc.10",
"content-type": "1.0.4", "content-type": "1.0.4",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
@ -36,7 +36,7 @@
"is-utf8": "0.2.1", "is-utf8": "0.2.1",
"js-yaml": "3.14.1", "js-yaml": "3.14.1",
"media-typer": "1.1.0", "media-typer": "1.1.0",
"mqtt": "4.2.8", "mqtt": "4.3.4",
"multer": "1.4.3", "multer": "1.4.3",
"mustache": "4.2.0", "mustache": "4.2.0",
"on-headers": "1.0.2", "on-headers": "1.0.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/registry", "name": "@node-red/registry",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,11 +16,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/util": "2.1.4", "@node-red/util": "2.1.6",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"semver": "7.3.5", "semver": "7.3.5",
"tar": "6.1.11", "tar": "6.1.11",
"uglify-js": "3.14.4" "uglify-js": "3.14.5"
} }
} }

View File

@ -83,6 +83,7 @@ function createNode(flow,config) {
} }
} }
try { try {
Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true })
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true }) Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
newNode = new nodeTypeConstructor(conf); newNode = new nodeTypeConstructor(conf);
} catch (err) { } catch (err) {

View File

@ -59,6 +59,9 @@ function Node(n) {
// which we can tolerate as they are the same object. // which we can tolerate as they are the same object.
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true }) Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
} }
if (n._module) {
Object.defineProperty(this,'_module', {value: n._module, enumerable: false, writable: true })
}
this.updateWires(n.wires); this.updateWires(n.wires);
} }
@ -496,7 +499,12 @@ function log_helper(self, level, msg) {
if (self.name) { if (self.name) {
o.name = self.name; o.name = self.name;
} }
self._flow.log(o); // See https://github.com/node-red/node-red/issues/3327
try {
self._flow.log(o);
} catch(err) {
logUnexpectedError(self, err)
}
} }
/** /**
* Log an INFO level message * Log an INFO level message
@ -576,4 +584,59 @@ Node.prototype.status = function(status) {
this._flow.handleStatus(this,status); this._flow.handleStatus(this,status);
}; };
function inspectObject(flow) {
try {
let properties = new Set()
let currentObj = flow
do {
if (!Object.getPrototypeOf(currentObj)) { break }
Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
} while ((currentObj = Object.getPrototypeOf(currentObj)))
let propList = [...properties.keys()].map(item => `${item}[${(typeof flow[item])[0]}]`)
propList.sort();
let result = [];
let line = "";
while (propList.length > 0) {
let prop = propList.shift()
if (line.length+prop.length > 80) {
result.push(line)
line = "";
} else {
line += " "+prop
}
}
if (line.length > 0) {
result.push(line);
}
return result.join("\n ")
} catch(err) {
return "Failed to capture object properties: "+err.toString()
}
}
function logUnexpectedError(node, error) {
let moduleInfo = node._module?`${node._module.module}@${node._module.version}`:"undefined"
Log.error(`
********************************************************************
Unexpected Node Error
${error.stack}
Node:
Type: ${node.type}
Module: ${moduleInfo}
ID: ${node._alias||node.id}
Properties:
${inspectObject(node)}
Flow: ${node._flow?node._flow.path:'undefined'}
Type: ${node._flow?node._flow.TYPE:'undefined'}
Properties:
${node._flow?inspectObject(node._flow):'undefined'}
Please report this issue, including the information logged above:
https://github.com/node-red/node-red/issues/
********************************************************************
`)
}
module.exports = Node; module.exports = Node;

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/runtime", "name": "@node-red/runtime",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"repository": { "repository": {
@ -16,11 +16,11 @@
} }
], ],
"dependencies": { "dependencies": {
"@node-red/registry": "2.1.4", "@node-red/registry": "2.1.6",
"@node-red/util": "2.1.4", "@node-red/util": "2.1.6",
"async-mutex": "0.3.2", "async-mutex": "0.3.2",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.17.1", "express": "4.17.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"json-stringify-safe": "5.0.1" "json-stringify-safe": "5.0.1"
} }

View File

@ -32,8 +32,14 @@ function wrapEventFunction(obj,func) {
return function(eventName, listener) { return function(eventName, listener) {
if (deprecatedEvents.hasOwnProperty(eventName)) { if (deprecatedEvents.hasOwnProperty(eventName)) {
const log = require("@node-red/util").log; const log = require("@node-red/util").log;
const stack = (new Error().stack).split("\n")[2].split("(")[1].slice(0,-1);
log.warn(`[RED.events] Deprecated use of "${eventName}" event from "${stack}". Use "${deprecatedEvents[eventName]}" instead.`) const stack = (new Error().stack).split("\n");
let location = "(unknown)"
// See https://github.com/node-red/node-red/issues/3292
if (stack.length > 2) {
location = stack[2].split("(")[1].slice(0,-1);
}
log.warn(`[RED.events] Deprecated use of "${eventName}" event from "${location}". Use "${deprecatedEvents[eventName]}" instead.`)
} }
return events["_"+func].call(events,eventName,listener) return events["_"+func].call(events,eventName,listener)
} }

View File

@ -686,7 +686,7 @@ function prepareJSONataExpression(value,node) {
return moment(arg1, arg2, arg3, arg4); return moment(arg1, arg2, arg3, arg4);
}); });
expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr.registerFunction('clone', cloneMessage, '<(oa)-:o>');
expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); expr._legacyMode = /(^|[^a-zA-Z0-9_'".])msg([^a-zA-Z0-9_'"]|$)/.test(value);
expr._node = node; expr._node = node;
return expr; return expr;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@node-red/util", "name": "@node-red/util",
"version": "2.1.4", "version": "2.1.6",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,7 +16,7 @@
], ],
"dependencies": { "dependencies": {
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"i18next": "21.5.4", "i18next": "21.6.6",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.5", "jsonata": "1.8.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red", "name": "node-red",
"version": "2.1.4", "version": "2.1.6",
"description": "Low-code programming for event-driven applications", "description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org", "homepage": "http://nodered.org",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -31,13 +31,13 @@
"flow" "flow"
], ],
"dependencies": { "dependencies": {
"@node-red/editor-api": "2.1.4", "@node-red/editor-api": "2.1.6",
"@node-red/runtime": "2.1.4", "@node-red/runtime": "2.1.6",
"@node-red/util": "2.1.4", "@node-red/util": "2.1.6",
"@node-red/nodes": "2.1.4", "@node-red/nodes": "2.1.6",
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"express": "4.17.1", "express": "4.17.2",
"fs-extra": "10.0.0", "fs-extra": "10.0.0",
"node-red-admin": "^2.2.1", "node-red-admin": "^2.2.1",
"nopt": "5.0.0", "nopt": "5.0.0",

View File

@ -0,0 +1,539 @@
var should = require("should");
var helper = require("node-red-node-test-helper");
var testNode = require("nr-test-utils").require("@node-red/nodes/core/function/rbe.js");
describe('rbe node', function() {
"use strict";
beforeEach(function(done) {
helper.startServer(done);
});
afterEach(function(done) {
helper.unload().then(function() {
helper.stopServer(done);
});
});
it("should be loaded with correct defaults", function(done) {
var flow = [{"id":"n1", "type":"rbe", "name":"rbe1", "wires":[[]]}];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
n1.should.have.property("name", "rbe1");
n1.should.have.property("func", "rbe");
n1.should.have.property("gap", "0");
done();
});
});
it('should only send output if payload changes - with multiple topics (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", 2);
c+=1;
}
else if (c == 2) {
msg.should.have.a.property("payload");
msg.payload.should.have.a.property("b",1);
msg.payload.should.have.a.property("c",2);
c+=1;
}
else if (c == 3) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 4) {
msg.should.have.a.property("payload",false);
c+=1;
}
else if (c == 5) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 6) {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",1);
c+=1;
}
else if (c == 7) {
msg.should.have.a.property("topic","b");
msg.should.have.a.property("payload",1);
c+=1;
}
else {
c += 1;
msg.should.have.a.property("topic","c");
msg.should.have.a.property("payload",1);
done();
}
});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:2});
n1.emit("input", {payload:2});
n1.emit("input", {payload:{b:1,c:2}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:true});
n1.emit("input", {payload:false});
n1.emit("input", {payload:false});
n1.emit("input", {payload:true});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"c",payload:1});
});
});
it('should ignore multiple topics if told to (rbe)', function(done) {
var flow = [{id:"n1", type:"rbe", func:"rbe", gap:"0", septopics:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", 2);
c+=1;
}
else if (c == 2) {
msg.should.have.a.property("payload");
msg.payload.should.have.a.property("b",1);
msg.payload.should.have.a.property("c",2);
c+=1;
}
else if (c == 3) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 4) {
msg.should.have.a.property("payload",false);
c+=1;
}
else if (c == 5) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 6) {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",1);
c+=1;
}
else {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",2);
done();
}
});
n1.emit("input", {topic:"a",payload:"a"});
n1.emit("input", {topic:"b",payload:"a"});
n1.emit("input", {topic:"c",payload:"a"});
n1.emit("input", {topic:"a",payload:2});
n1.emit("input", {topic:"b",payload:2});
n1.emit("input", {payload:{b:1,c:2}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {topic:"a",payload:true});
n1.emit("input", {topic:"b",payload:false});
n1.emit("input", {topic:"c",payload:false});
n1.emit("input", {topic:"d",payload:true});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"c",payload:1});
n1.emit("input", {topic:"d",payload:1});
n1.emit("input", {topic:"a",payload:2});
});
});
it('should only send output if another chosen property changes - foo (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", property:"foo", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("foo", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("foo", "b");
c+=1;
}
else {
msg.should.have.a.property("foo");
msg.foo.should.have.a.property("b",1);
msg.foo.should.have.a.property("c",2);
done();
}
});
n1.emit("input", {foo:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {foo:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {foo:"a"});
n1.emit("input", {foo:"b"});
n1.emit("input", {foo:{b:1,c:2}});
n1.emit("input", {foo:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
});
});
it('should only send output if payload changes - ignoring first value (rbei)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbei", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "b");
msg.should.have.a.property("topic", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", "b");
msg.should.have.a.property("topic", "b");
c+=1;
}
else if (c === 2) {
msg.should.have.a.property("payload", "c");
msg.should.have.a.property("topic", "a");
c+=1;
}
else {
msg.should.have.a.property("payload", "c");
msg.should.have.a.property("topic", "b");
done();
}
});
n1.emit("input", {payload:"a", topic:"a"});
n1.emit("input", {payload:"a", topic:"b"});
n1.emit("input", {payload:"a", topic:"a"});
n1.emit("input", {payload:"b", topic:"a"});
n1.emit("input", {payload:"b", topic:"b"});
n1.emit("input", {payload:"c", topic:"a"});
n1.emit("input", {payload:"c", topic:"b"});
});
});
it('should send output if queue is reset (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 2) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 3) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 4) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 5) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 6) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else {
msg.should.have.a.property("payload", "c");
done();
}
});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {reset:true}); // reset all
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"b", reset:""}); // reset b
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {reset:""}); // reset all
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"c"}); // don't reset a non topic
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"c", payload:"c"});
});
});
it('should only send output if x away from original value (deadbandEq)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadbandEq", gap:"10", inout:"out", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 10);
}
else if (c == 3) {
msg.should.have.a.property("payload", 20);
done();
}
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:2});
n1.emit("input", {payload:4});
n1.emit("input", {payload:6});
n1.emit("input", {payload:8});
n1.emit("input", {payload:10});
n1.emit("input", {payload:15});
n1.emit("input", {payload:20});
});
});
it('should only send output if more than x away from original value (deadband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
//console.log(c,msg);
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 20);
}
else {
msg.should.have.a.property("payload", "5 deg");
done();
}
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:2});
n1.emit("input", {payload:4});
n1.emit("input", {payload:"6 deg"});
n1.emit("input", {payload:8});
n1.emit("input", {payload:20});
n1.emit("input", {payload:15});
n1.emit("input", {payload:"5 deg"});
});
});
it('should only send output if more than x% away from original value (deadband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10%", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
if (c === 1) {
msg.should.have.a.property("payload", 100);
}
else if (c === 2) {
msg.should.have.a.property("payload", 111);
}
else if (c === 3) {
msg.should.have.a.property("payload", 135);
done();
}
});
n1.emit("input", {payload:100});
n1.emit("input", {payload:95});
n1.emit("input", {payload:105});
n1.emit("input", {payload:111});
n1.emit("input", {payload:120});
n1.emit("input", {payload:135});
});
});
it('should warn if no number found in deadband mode', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c += 1;
});
setTimeout( function() {
c.should.equal(0);
helper.log().called.should.be.true;
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "rbe";
});
logEvents.should.have.length(1);
var msg = logEvents[0][0];
msg.should.have.property('level', helper.log().WARN);
msg.should.have.property('id', 'n1');
msg.should.have.property('type', 'rbe');
msg.should.have.property('msg', 'rbe.warn.nonumber');
done();
},50);
n1.emit("input", {payload:"banana"});
});
});
it('should not send output if x away or greater from original value (narrowbandEq)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowbandEq", gap:"10", inout:"out", start:"1", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
//console.log(c,msg);
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 5);
}
else if (c === 3) {
msg.should.have.a.property("payload", 10);
done();
}
});
n1.emit("input", {payload:100});
n1.emit("input", {payload:0});
n1.emit("input", {payload:10});
n1.emit("input", {payload:5});
n1.emit("input", {payload:15});
n1.emit("input", {payload:10});
n1.emit("input", {payload:20});
n1.emit("input", {payload:25});
});
});
it('should not send output if more than x away from original value (narrowband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 0);
}
else if (c === 1) {
msg.should.have.a.property("payload","6 deg");
}
else {
msg.should.have.a.property("payload", "5 deg");
done();
}
c += 1;
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:20});
n1.emit("input", {payload:40});
n1.emit("input", {payload:"6 deg"});
n1.emit("input", {payload:18});
n1.emit("input", {payload:20});
n1.emit("input", {payload:50});
n1.emit("input", {payload:"5 deg"});
});
});
it('should send output if gap is 0 and input doesnt change (narrowband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 1);
}
else if (c === 4) {
msg.should.have.a.property("payload",1);
done();
}
c += 1;
});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:0});
n1.emit("input", {payload:1});
});
});
it('should not send output if more than x away from original value (narrowband in step mode)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"10", inout:"in", start:"500", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 55);
}
else if (c === 1) {
msg.should.have.a.property("payload", 205);
done();
}
c += 1;
});
n1.emit("input", {payload:50});
n1.emit("input", {payload:55});
n1.emit("input", {payload:200});
n1.emit("input", {payload:205});
});
});
});