├── .gitattributes ├── .github └── workflows │ ├── ci-build.yml │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── components ├── component-return.html ├── component-return.js ├── component-start.html ├── component-start.js ├── emitter.js ├── examples │ ├── basic.json │ └── embedded.json ├── locales │ ├── de │ │ ├── component-return.html │ │ ├── component-return.json │ │ ├── component-start.html │ │ ├── component-start.json │ │ ├── run-component.html │ │ └── run-component.json │ └── en-US │ │ ├── component-return.html │ │ ├── component-return.json │ │ ├── component-start.html │ │ ├── component-start.json │ │ ├── run-component.html │ │ └── run-component.json ├── run-component.html ├── run-component.js ├── test │ ├── bugfix_missing_mode_prop_spec.js │ ├── multiple_out_ports_spec.js │ ├── nested_local_global_spec.js │ ├── nested_spec.js │ ├── param_validation_spec.js │ └── unconnected_spec.js └── uitest │ ├── editor_spec.js │ ├── flows.json │ ├── package.json │ └── settings.js ├── examples ├── basic.json ├── broadcast.json ├── embedded.json ├── in-only.json └── recursion.json ├── images ├── component.png ├── component_in.png ├── component_out.png └── components.png └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-vendored 2 | *.json linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build and run tests using node 2 | 3 | name: ci build 4 | 5 | on: 6 | push: 7 | branches-ignore: 8 | - 'master' 9 | pull_request: 10 | branches-ignore: 11 | - 'master' 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - run: ls -la 18 | - run: npm install 19 | - run: npm test 20 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to the NPM repo 2 | 3 | name: Publish to NPM 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: ls -la 15 | - run: npm install 16 | - run: npm test 17 | publish: 18 | runs-on: ubuntu-latest 19 | needs: test 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: npm install 24 | - name: Publish 25 | uses: mikeal/merge-release@master 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 29 | SRC_PACKAGE_DIR: / 30 | DEPLOY_DIR: / -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | 108 | components/uitest/*.png 109 | components/uitest/.* 110 | components/uitest/mode_modules/* 111 | 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ollixx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-components 2 | Components are an alternative approach to create reusable node-red flows and are 3 | very much inspired by [action flows](https://github.com/Steveorevo/node-red-contrib-actionflows/tree/master/actionflows). 4 | 5 | ![Components](/images/components.png) 6 | 7 | ## Motivation 8 | Some projects can get really complicated and having 20 tabs with hundreds of nodes needs some more structure. Components always 9 | have been in my head, despite subflows doing their job really well. But the later tend to be more cumbersome and less flexible. 10 | [Action flows](https://github.com/Steveorevo/node-red-contrib-actionflows/tree/master/actionflows) are another very nice 11 | approach to encapsulate (business) logic and make it reusable. But their definition of an API is not mine. 12 | So here it is: An advanced set of nodes, that will hopefully help you to organize your flows. 13 | 14 | * Components encapsulate well defined logic and tasks in a way that let's you keep track of what it is doing. 15 | * Define once, use many times 16 | * Components are a very compact way to prepare the ```msg```, pass it to a "black box" flow and get it back with just 17 | the parts you need. 18 | * Components make their API truly visible in that they define a set of msg parts, that are expected or optional. The 19 | calling node can prepare all the input parts just in one nice list, that is derived from the component's parameters. 20 | 21 | ## The Nodes 22 | ### component_in - Start of a Component flow 23 | ![Component input node](/images/component_in.png) 24 | 25 | This input node is the starting point of an reusable flow. It allows to set a list of parameters, that it expects. 26 | Every parameter is defined by its name, a type and a flag to define it as required or optional. This list of parameters can be seen as the API of the component. 27 | 28 | ### component_out - End of the flow 29 | ![Component output node](/images/component_out.png) 30 | 31 | This node returns the ```msg``` to the calling node and supports nested components. 32 | 33 | If it receives a message, that is not created by a calling compontent node, the message is broadcast to all possible callers, i.e. all component nodes, that use the return node's flow. 34 | 35 | A component flow can have more than one return node. For each of them, the calling component node optionally features a separate output port labeled with the name of the return node. By default, a return node will send its message to one "default" output port. 36 | 37 | In versions to come, it will 38 | also allow to set some options like purging unwanted or temporary parts from the final ```msg```. 39 | 40 | ### component - the calling node 41 | 42 | ![Component caller node](/images/component.png) 43 | 44 | To use a prepared Component, this node is setup. It must be configured in accordance with the Component's parameter list, so 45 | that at least all required ```msg``` parts are connected to either of the known options also presented in the honorable change node: 46 | * parts in msg, flow context, global context 47 | * constants of types boolean, number, string, timestamp, Buffer and json 48 | * jsonata or regular expressions 49 | * environment variables 50 | * other nodes 51 | 52 | Until now only required parameters are validated against null, undefined and empty string. More validation is one the todo list. 53 | 54 | ### Install 55 | node-red-contrib-components can be install using the node-red editor's pallete or by running npm in the console: 56 | 57 | ``` bash 58 | npm install node-red-contrib-components 59 | ``` 60 | 61 | ### Examples 62 | There are some examples available now in the node-red specific way. Just select import... in the menu and find ```node-red-contrib-components``` in the exmaples sections. Beside some basic flows, that show how to use components, there are some more advanced scenarios to show speecific features like broadcast, in-only etc. Check it out. 63 | 64 | #### Cut/Copy/Paste component nodes 65 | At the moment only ```use comp``` can be copy/pasted wihtout problems. 66 | Cut/Copy/Pasting component flows, i.e. ```comp start``` and ```comp return``` nodes together with any linked ```use comp``` nodes will not preserve the association between them. 67 | There is a work around to move (not copy) components together with their ```use comp``` nodes: Export the set of nodes, remove them from NR and re-import them at the new tab. 68 | At least the Cut/Paste behaviour will probably be fixed in one of the next releases. 69 | 70 | ### Component features in detail 71 | * multiple output ports. 72 | 73 | When using more than one ```component_return``` node, the caller node automatically exposes each of them as a separate output port. This can be statically analysed by parsing the ```component_in``` node's wires. The ```component_return``` node has a new property "Output Ports", that can be set to "separate" or "default". In the later case, the "default" output port is used (for all return nodes on "default") 74 | * Allow to set the status in the ```component``` node. See: https://github.com/ollixx/node-red-contrib-components/issues/1 75 | * Validate incoming message parts in the ```component``` node. Use the types defined in the API to validate the message parts. See https://github.com/ollixx/node-red-contrib-components/issues/2 76 | * Context Message Part & local/global parameters 77 | 78 | Each component flow exposes a special `msg.component` part, that contains all local parameters. By default, all parameters are local. There is an option in the API to change a parameter to be "global". In that case it will be put into the `msg`directly and stay there, even when the flow is left. 79 | 80 | `msg.component` has another part embedded, `msg.component._parent` if the current flow is nested inside another. That supports a stack for nesting and resursive flows. 81 | 82 | ## Ideas 83 | * filter the incoming message, so certain parts are purged before executing the component 84 | * parameter setting "pass through". If set, the message part is hidden inside the component flow. Its initial value is overwritten by the value set in the caller node. After the return, the message part is restored with the initial, passed through value. The value is stored inside the ```_comp``` message part, that transports component meta data. (Maybe outdate with `msg.component`?) 85 | * parameter setting "purge". If set, the message part is removed before the message is returned to the caller node. (maybe outdated by `msg.component`). 86 | * parameter setting "validate". Opens a new line of extra settings for type specific validations (jsonata?) 87 | * parameter setting "default". Set a default value both for optional and required parameters. 88 | * parameter setting "description". write a few infos about what the parameter does in the component. 89 | * top level description. Some more words about the purpose of the whole component. 90 | * support more parameter types ( Buffer, ... ) 91 | * support json schema. Could be used as a new parameter type or for validation. 92 | * have an optional, additional ouput port on the caller node, that is used on validation errors (e.g. required param missing). 93 | * allow definition of outgoing message parts in ```component_out```. This might be an alternative to setting "purge" or "pass through" in ```component_in```. This could be seen as the outbound API, as it defines, what the following flows can expect to get passed in the msg coming from the component. 94 | * use ```enum``` as another possible type for parameters. The enumeration values would have to be defined either in an editable list, an array of strings (either pasted in the parameter editor or set by editableType field) or an object (keys would be the enum values, but would allow to access a structured object for each enum value). 95 | -------------------------------------------------------------------------------- /components/component-return.html: -------------------------------------------------------------------------------- 1 | 4 | 127 | -------------------------------------------------------------------------------- /components/component-return.js: -------------------------------------------------------------------------------- 1 | const componentsEmitter = require("./emitter"); 2 | 3 | module.exports = function (RED) { 4 | 5 | const EVENT_START_FLOW = "comp-start-flow"; 6 | const EVENT_RETURN_FLOW = "comp-flow-return"; 7 | 8 | /** 9 | * Retrieve all nodes, that are wired to call the given nodeid. 10 | * Then retrieve the parent callers for each of the found nodes revcursively 11 | * This func is also aware of link nodes. 12 | * The retrieval list can be reduced by passing in a filter function 13 | */ 14 | function getCallerHierarchy(targetid, filter = null, visited = []) { 15 | let result = {} 16 | if (visited.includes(targetid)) { 17 | return result; 18 | } 19 | visited.push(targetid); 20 | RED.nodes.eachNode((child) => { 21 | if (child.wires) { 22 | child.wires.forEach((port) => { 23 | port.forEach((nodeid) => { 24 | if (nodeid == targetid) { 25 | // handle link nodes 26 | if (child.type == "link in") { 27 | let linkHierarchy = { 28 | node: child, 29 | callers: {} 30 | } 31 | child.links.forEach((linkOutId) => { 32 | RED.nodes.eachNode((foundNode) => { 33 | if (linkOutId == foundNode.id) { 34 | linkHierarchy.callers[linkOutId] = { 35 | node: foundNode, 36 | callers: getCallerHierarchy(linkOutId, filter, visited) 37 | } 38 | } 39 | }) 40 | }) 41 | result[child.id] = linkHierarchy 42 | } else { 43 | result[child.id] = { 44 | node: child, 45 | callers: getCallerHierarchy(child.id, filter, visited) 46 | } 47 | } 48 | } 49 | }) 50 | }) 51 | } 52 | }) 53 | return result 54 | } 55 | 56 | function isInvalidInSubflow(red, node) { 57 | let found = false 58 | RED.nodes.eachNode((n) => { 59 | if (n.id == node.z && n.type.startsWith("subflow")) { 60 | found = true 61 | } 62 | }) 63 | return found 64 | } 65 | 66 | /* 67 | ******* COMPONENT RETURN ************* 68 | third node: component out 69 | 70 | */ 71 | RED.nodes.registerType("component_out", componentOut); 72 | function componentOut(config) { 73 | // Create our node and event handler 74 | RED.nodes.createNode(this, config); 75 | var node = this; 76 | 77 | // fix legacy nodes without mode 78 | node.mode = config.mode || "default"; 79 | 80 | // look for the component IN that I belong to: 81 | let findInComponentNode = function (callers, found = {}) { 82 | Object.entries(callers).forEach(([id, entry]) => { 83 | if (entry.node.type == "component_in") { 84 | found[id] = entry.node; 85 | } else { 86 | found = { ...found, ...findInComponentNode(entry.callers) } 87 | } 88 | }) 89 | return found; 90 | } 91 | 92 | let callers = getCallerHierarchy(node.id) 93 | let foundInNodes = findInComponentNode(callers) 94 | node.inNodeLength = Object.keys(foundInNodes).length 95 | if (node.inNodeLength != 1) { 96 | node.error(RED._("components.message.returnWithoutStart", { inNodeLength: node.inNodeLength })) 97 | node.invalid = true 98 | } else { 99 | node.inNode = Object.values(foundInNodes)[0] 100 | } 101 | 102 | if (isInvalidInSubflow(RED, node) == true) { 103 | node.error(RED._("components.message.componentInSubflow")) 104 | return 105 | } 106 | 107 | this.on("input", function (msg) { 108 | try { 109 | if (isInvalidInSubflow(RED, node) == true) { 110 | node.error(RED._("components.message.componentInSubflow")) 111 | return 112 | } 113 | 114 | if (node.invalid) { 115 | node.error(RED._("components.message.returnWithoutStart", { inNodeLength: node.inNodeLength })) 116 | return // stop execution here 117 | } 118 | 119 | // create / update state for new execution 120 | if (msg._comp !== undefined) { 121 | // peek into stack to know where to return: 122 | let entry = msg._comp.stack.slice(-1)[0]; 123 | msg._comp.returnNode = { 124 | id: node.id, 125 | callerId: entry.callerId, // prevent unwanted return chain 126 | mode: node.mode, 127 | name: node.name 128 | } 129 | // send event 130 | componentsEmitter.emit(EVENT_RETURN_FLOW + "-" + entry.callerId, msg); 131 | } else { 132 | // broadcast the message to all RUN node 133 | try { 134 | RED.nodes.eachNode((runNode) => { 135 | if (runNode.type == "component") { 136 | let targetComponent = RED.nodes.getNode(runNode.targetComponentId); 137 | // legacy 138 | if (!targetComponent) { 139 | console.log("legacy", runNode); 140 | console.log("_comp?", msg._comp); 141 | targetComponent = RED.nodes.getNode(runNode.targetComponent.id); 142 | } 143 | 144 | if (targetComponent && targetComponent.id == node.inNode.id) { 145 | if (msg._comp === undefined) { 146 | msg._comp = { 147 | stack: [] 148 | }; 149 | } 150 | msg._comp.target = targetComponent.id; 151 | let stackEntry = { callerId: runNode.id, targetId: targetComponent.id }; 152 | if (targetComponent && targetComponent.usecontext) { 153 | stackEntry.context = {} 154 | } 155 | msg._comp.stack.push(stackEntry) 156 | msg._comp.returnNode = { 157 | id: node.id, 158 | mode: node.mode, 159 | name: node.name, 160 | broadcast: true 161 | } 162 | componentsEmitter.emit(EVENT_RETURN_FLOW + "-" + runNode.id, msg); 163 | } 164 | } 165 | }); 166 | } catch (err) { 167 | console.trace(node.name || node.type, node.id, err) 168 | node.error("err in out: " + err); 169 | } 170 | } 171 | } catch (err) { 172 | console.trace(err) 173 | node.error("err in return", err) 174 | // console.trace() 175 | } 176 | }); // END: on input 177 | 178 | } // END: COMPONENT RETURN 179 | 180 | }; // end module.exports 181 | -------------------------------------------------------------------------------- /components/component-start.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 189 | -------------------------------------------------------------------------------- /components/component-start.js: -------------------------------------------------------------------------------- 1 | const componentsEmitter = require("./emitter"); 2 | 3 | module.exports = function (RED) { 4 | 5 | const EVENT_START_FLOW = "comp-start-flow"; 6 | const EVENT_RETURN_FLOW = "comp-flow-return"; 7 | 8 | function isInvalidInSubflow(red, node) { 9 | let found = false 10 | RED.nodes.eachNode((n) => { 11 | if (n.id == node.z && n.type.startsWith("subflow")) { 12 | found = true 13 | } 14 | }) 15 | return found 16 | } 17 | 18 | // find all RETURN component nodes, that are connected to me. 19 | // traverses all connected nodes, including link nodes 20 | const findReturnNodes = function (nodeid, foundNodes, type = "component_out", visited = []) { 21 | if (visited.includes(nodeid)) { 22 | // already been here, so quit 23 | return; 24 | } 25 | visited.push(nodeid); // mark as vistited 26 | try { 27 | let node = RED.nodes.getNode(nodeid); 28 | if (!node) { 29 | throw "could not find node for id" + nodeid; 30 | } 31 | if (node.wires && node.wires.length > 0) { 32 | node.wires.forEach((outPort) => { 33 | outPort.forEach((childid) => { 34 | let child = RED.nodes.getNode(childid); 35 | if (!child) { 36 | throw "could not find child node for id" + childid; 37 | } 38 | if (child.type == type) { 39 | foundNodes[childid] = child; 40 | } else if (child.type == "link out") { 41 | // look for more nodes at the other side of the link 42 | if (child.links) { 43 | // old nr: 44 | child.links.forEach((linkid) => { 45 | findReturnNodes(linkid, foundNodes, type, visited) 46 | }) 47 | } else if (child.wires) { 48 | // nr2 49 | child.wires[0].forEach((linkid) => { 50 | findReturnNodes(linkid, foundNodes, type, visited) 51 | }) 52 | } 53 | } 54 | // look for connected nodes 55 | findReturnNodes(childid, foundNodes, type, visited) 56 | }) 57 | }); 58 | } 59 | } catch (err) { 60 | /* 61 | console.log("-----------------------------/n error in first nodeid", visited[0]); 62 | console.log(" visited", visited); 63 | console.trace(err) 64 | //*/ 65 | } 66 | } 67 | 68 | /* 69 | 70 | ******* COMPONENT START ************* 71 | first node: componet in 72 | 73 | */ 74 | RED.nodes.registerType("component_in", componentIn); 75 | function componentIn(config) { 76 | // Create our node and event handler 77 | RED.nodes.createNode(this, config); 78 | var node = this; 79 | 80 | node.usecontext = config.usecontext || false; 81 | node.api = config.api; // keep in the node to let RUN nodes find it at runtime (after changes in START only) 82 | 83 | var startFlowHandler = function (msg) { 84 | try { 85 | if (isInvalidInSubflow(RED, node) == true) { 86 | node.error("component defintion is not allowed in subflow.") 87 | return 88 | } 89 | let target = msg._comp ? msg._comp.target : undefined; 90 | if (target == node.id) { 91 | delete msg._comp.target; // remove flag to start this node. 92 | node.receive(msg); 93 | } 94 | } catch (err) { 95 | console.trace(err) 96 | node.error(err) 97 | } 98 | } 99 | componentsEmitter.on(EVENT_START_FLOW + "-" + node.id, startFlowHandler); 100 | 101 | // Clean up event handler 102 | this.on("close", function () { 103 | componentsEmitter.removeListener(EVENT_START_FLOW + "-" + node.id, startFlowHandler); 104 | }); 105 | 106 | this.on("input", function (msg) { 107 | if (node.invalid) { 108 | node.error("component not allowed in subflow") 109 | return 110 | } 111 | let stack = msg._comp.stack; 112 | let lastEntry = stack.slice(-1)[0]; 113 | node.status({ fill: "grey", shape: "ring", text: RED._("components.message.lastCaller") + ": " + lastEntry.callerId }); 114 | this.send(msg); 115 | 116 | // If this START node is not connected to a return node, we send back a notification to the calling RUN node, so it can continue. 117 | let foundReturnNodes = {} 118 | findReturnNodes(node.id, foundReturnNodes) 119 | if (Object.keys(foundReturnNodes).length == 0) { 120 | // send event to caller, so he can finish his "running" state 121 | componentsEmitter.emit(EVENT_RETURN_FLOW + "-" + lastEntry.callerId, msg); 122 | } 123 | }); 124 | 125 | } // END: COMPONENT IN 126 | 127 | }; // end module.exports 128 | -------------------------------------------------------------------------------- /components/emitter.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class ComponentsEmitter extends EventEmitter { } 4 | const componentsEmitter = new ComponentsEmitter() 5 | componentsEmitter.setMaxListeners(0) // remove "MaxListenersExceededWarning" 6 | module.exports = componentsEmitter; -------------------------------------------------------------------------------- /components/examples/basic.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "fbb8e0c6.fd63c", 4 | "type": "component_in", 5 | "z": "5384bb85.537fd4", 6 | "name": "Component", 7 | "api": [], 8 | "x": 170, 9 | "y": 640, 10 | "wires": [ 11 | [ 12 | "19eab3c2.0c8d8c" 13 | ] 14 | ] 15 | }, 16 | { 17 | "id": "19eab3c2.0c8d8c", 18 | "type": "component_out", 19 | "z": "5384bb85.537fd4", 20 | "name": "return", 21 | "mode": "default", 22 | "x": 450, 23 | "y": 640, 24 | "wires": [] 25 | }, 26 | { 27 | "id": "65025951.68c2a8", 28 | "type": "component", 29 | "z": "5384bb85.537fd4", 30 | "name": "run it", 31 | "targetComponent": { 32 | "id": "fbb8e0c6.fd63c", 33 | "name": "Component", 34 | "api": [] 35 | }, 36 | "paramSources": {}, 37 | "statuz": "", 38 | "statuzType": "str", 39 | "outputs": 1, 40 | "outLabels": [ 41 | "default" 42 | ], 43 | "x": 290, 44 | "y": 600, 45 | "wires": [ 46 | [ 47 | "364e4d91.1c1952" 48 | ] 49 | ] 50 | }, 51 | { 52 | "id": "364e4d91.1c1952", 53 | "type": "debug", 54 | "z": "5384bb85.537fd4", 55 | "name": "", 56 | "active": true, 57 | "tosidebar": true, 58 | "console": false, 59 | "tostatus": false, 60 | "complete": "true", 61 | "targetType": "full", 62 | "statusVal": "", 63 | "statusType": "auto", 64 | "x": 450, 65 | "y": 600, 66 | "wires": [] 67 | }, 68 | { 69 | "id": "4398a730.4ea3e8", 70 | "type": "inject", 71 | "z": "5384bb85.537fd4", 72 | "name": "Run", 73 | "props": [ 74 | { 75 | "p": "payload" 76 | } 77 | ], 78 | "repeat": "", 79 | "crontab": "", 80 | "once": false, 81 | "onceDelay": 0.1, 82 | "topic": "", 83 | "payload": "Works!", 84 | "payloadType": "str", 85 | "x": 150, 86 | "y": 600, 87 | "wires": [ 88 | [ 89 | "65025951.68c2a8" 90 | ] 91 | ] 92 | } 93 | ] -------------------------------------------------------------------------------- /components/examples/embedded.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dcdfa483.b6b2d8", 4 | "type": "component_in", 5 | "z": "5384bb85.537fd4", 6 | "name": "Component 1", 7 | "api": [ 8 | { 9 | "name": "name1", 10 | "type": "string", 11 | "required": true 12 | }, 13 | { 14 | "name": "Das ist ein längerer", 15 | "type": "json", 16 | "required": true 17 | }, 18 | { 19 | "name": "number", 20 | "type": "number", 21 | "required": true 22 | }, 23 | { 24 | "name": "bool", 25 | "type": "boolean", 26 | "required": true 27 | }, 28 | { 29 | "name": "sdfsdfdfsd", 30 | "type": "any", 31 | "required": true 32 | } 33 | ], 34 | "x": 170, 35 | "y": 320, 36 | "wires": [ 37 | [ 38 | "f18f31a4.2888b" 39 | ] 40 | ] 41 | }, 42 | { 43 | "id": "56b92b19.e59524", 44 | "type": "component_out", 45 | "z": "5384bb85.537fd4", 46 | "name": "ret 01a", 47 | "mode": "default", 48 | "x": 710, 49 | "y": 320, 50 | "wires": [] 51 | }, 52 | { 53 | "id": "dc96eeae.03b13", 54 | "type": "component", 55 | "z": "5384bb85.537fd4", 56 | "name": "run 02", 57 | "targetComponent": { 58 | "id": "30f5fb76.8401c4", 59 | "name": "Component 2", 60 | "api": [] 61 | }, 62 | "paramSources": {}, 63 | "statuz": "", 64 | "statuzType": "str", 65 | "outputs": 1, 66 | "outLabels": [ 67 | "ret 02" 68 | ], 69 | "x": 550, 70 | "y": 320, 71 | "wires": [ 72 | [ 73 | "56b92b19.e59524" 74 | ] 75 | ] 76 | }, 77 | { 78 | "id": "30f5fb76.8401c4", 79 | "type": "component_in", 80 | "z": "5384bb85.537fd4", 81 | "name": "Component 2", 82 | "api": [], 83 | "x": 170, 84 | "y": 420, 85 | "wires": [ 86 | [ 87 | "f894bb4.9489448" 88 | ] 89 | ] 90 | }, 91 | { 92 | "id": "ead6ba0d.80dd18", 93 | "type": "component_out", 94 | "z": "5384bb85.537fd4", 95 | "name": "ret 02", 96 | "mode": "separate", 97 | "x": 670, 98 | "y": 420, 99 | "wires": [] 100 | }, 101 | { 102 | "id": "f18f31a4.2888b", 103 | "type": "change", 104 | "z": "5384bb85.537fd4", 105 | "name": "", 106 | "rules": [ 107 | { 108 | "t": "set", 109 | "p": "outer", 110 | "pt": "msg", 111 | "to": "{\"test\": 42}", 112 | "tot": "json" 113 | } 114 | ], 115 | "action": "", 116 | "property": "", 117 | "from": "", 118 | "to": "", 119 | "reg": false, 120 | "x": 360, 121 | "y": 320, 122 | "wires": [ 123 | [ 124 | "dc96eeae.03b13", 125 | "1c65437f.5d316d" 126 | ] 127 | ] 128 | }, 129 | { 130 | "id": "8b154ce9.4db91", 131 | "type": "inject", 132 | "z": "5384bb85.537fd4", 133 | "name": "Run", 134 | "props": [ 135 | { 136 | "p": "payload" 137 | } 138 | ], 139 | "repeat": "", 140 | "crontab": "", 141 | "once": false, 142 | "onceDelay": 0.1, 143 | "topic": "", 144 | "payload": "{\"inner\":{\"more\":\"Hey\",\"even more\":999}}", 145 | "payloadType": "json", 146 | "x": 170, 147 | "y": 240, 148 | "wires": [ 149 | [ 150 | "1cf63259.f760be" 151 | ] 152 | ] 153 | }, 154 | { 155 | "id": "5900835f.17f83c", 156 | "type": "debug", 157 | "z": "5384bb85.537fd4", 158 | "name": "default (inner)", 159 | "active": true, 160 | "tosidebar": true, 161 | "console": false, 162 | "tostatus": false, 163 | "complete": "true", 164 | "targetType": "full", 165 | "statusVal": "", 166 | "statusType": "auto", 167 | "x": 520, 168 | "y": 220, 169 | "wires": [] 170 | }, 171 | { 172 | "id": "f894bb4.9489448", 173 | "type": "change", 174 | "z": "5384bb85.537fd4", 175 | "name": "", 176 | "rules": [ 177 | { 178 | "t": "set", 179 | "p": "inner", 180 | "pt": "msg", 181 | "to": "23", 182 | "tot": "json" 183 | } 184 | ], 185 | "action": "", 186 | "property": "", 187 | "from": "", 188 | "to": "", 189 | "reg": false, 190 | "x": 360, 191 | "y": 420, 192 | "wires": [ 193 | [ 194 | "f250b246.6d27c" 195 | ] 196 | ] 197 | }, 198 | { 199 | "id": "1c65437f.5d316d", 200 | "type": "component_out", 201 | "z": "5384bb85.537fd4", 202 | "name": "ret 01b", 203 | "mode": "separate", 204 | "x": 560, 205 | "y": 360, 206 | "wires": [] 207 | }, 208 | { 209 | "id": "1cf63259.f760be", 210 | "type": "component", 211 | "z": "5384bb85.537fd4", 212 | "name": "run 01", 213 | "targetComponent": { 214 | "id": "dcdfa483.b6b2d8", 215 | "name": "Component 1", 216 | "api": [ 217 | { 218 | "name": "name1", 219 | "type": "string", 220 | "required": true 221 | }, 222 | { 223 | "name": "Das ist ein längerer", 224 | "type": "json", 225 | "required": true 226 | }, 227 | { 228 | "name": "number", 229 | "type": "number", 230 | "required": true 231 | }, 232 | { 233 | "name": "bool", 234 | "type": "boolean", 235 | "required": true 236 | }, 237 | { 238 | "name": "sdfsdfdfsd", 239 | "type": "any", 240 | "required": true 241 | } 242 | ] 243 | }, 244 | "paramSources": { 245 | "name1": { 246 | "name": "name1", 247 | "type": "string", 248 | "required": true, 249 | "source": "\"Test\"", 250 | "sourceType": "jsonata" 251 | }, 252 | "Das ist ein längerer": { 253 | "name": "Das ist ein längerer", 254 | "type": "json", 255 | "required": true, 256 | "source": "[\"a\", \"b\"]", 257 | "sourceType": "json" 258 | }, 259 | "number": { 260 | "name": "number", 261 | "type": "number", 262 | "required": true, 263 | "source": "4", 264 | "sourceType": "json" 265 | }, 266 | "bool": { 267 | "name": "bool", 268 | "type": "boolean", 269 | "required": true, 270 | "source": "true", 271 | "sourceType": "bool" 272 | }, 273 | "sdfsdfdfsd": { 274 | "name": "sdfsdfdfsd", 275 | "type": "any", 276 | "required": true, 277 | "source": "{}", 278 | "sourceType": "json" 279 | } 280 | }, 281 | "statuz": "name1", 282 | "statuzType": "msg", 283 | "outputs": 2, 284 | "outLabels": [ 285 | "default", 286 | "ret 01b" 287 | ], 288 | "x": 330, 289 | "y": 240, 290 | "wires": [ 291 | [ 292 | "5900835f.17f83c" 293 | ], 294 | [ 295 | "ee4645bc.e3f438" 296 | ] 297 | ] 298 | }, 299 | { 300 | "id": "e2a3926c.6e823", 301 | "type": "link in", 302 | "z": "5384bb85.537fd4", 303 | "name": "link in 01", 304 | "links": [ 305 | "f250b246.6d27c" 306 | ], 307 | "x": 575, 308 | "y": 420, 309 | "wires": [ 310 | [ 311 | "ead6ba0d.80dd18" 312 | ] 313 | ] 314 | }, 315 | { 316 | "id": "f250b246.6d27c", 317 | "type": "link out", 318 | "z": "5384bb85.537fd4", 319 | "name": "link out 01", 320 | "links": [ 321 | "e2a3926c.6e823" 322 | ], 323 | "x": 475, 324 | "y": 420, 325 | "wires": [] 326 | }, 327 | { 328 | "id": "ee4645bc.e3f438", 329 | "type": "debug", 330 | "z": "5384bb85.537fd4", 331 | "name": "outer only", 332 | "active": true, 333 | "tosidebar": true, 334 | "console": false, 335 | "tostatus": false, 336 | "complete": "true", 337 | "targetType": "full", 338 | "statusVal": "", 339 | "statusType": "auto", 340 | "x": 500, 341 | "y": 260, 342 | "wires": [] 343 | } 344 | ] -------------------------------------------------------------------------------- /components/locales/de/component-return.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/locales/de/component-return.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Komponente", 6 | "name": "Name", 7 | "required": "erwartet", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Einzel" 13 | }, 14 | "outportChange": "Achtung", 15 | "componentInSubflow": "Nicht erlaubt in Subflows", 16 | "global": "global" 17 | }, 18 | "message": { 19 | "invalid_comp": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp' ist 'undefined' oder 'null'", 20 | "invalid_stack": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp.stack' ist 'undefined' oder 'null'", 21 | "invalid_idMatch": "Komponente '__nodeId__' empfing inkorrektes Event. ID stimmt nicht überein mit: __callerId__", 22 | "lastCaller": "letzter Aufruf von", 23 | "missingProperty": "Parameter '__parameter__' wird erwartet, hat aber keinen Wert.", 24 | "running": "läuft", 25 | "validationError": "Ungültiger Parameter '__parameter__'. Erwartet: '__expected__', Empfangen: '__invalidType__'. Wert: '__value__'.", 26 | "jsonValidationError": "Ungültiger Parameter '__parameter__'. Wert konnte nicht als JSON validiert werden: '__value__'.", 27 | "outportChange": "Die Output Ports haben sich geändert. Bitte diesen Dialog speichern, um die Anzahl und Bezeichnung der Ports zu aktualisieren.", 28 | "componentInSubflow": "Components dürfen nicht innerhalb von subflows definiert werden. Nur das Ausführen von Components ist erlaubt.", 29 | "componentNotConnected": "Für diesen Knoten muss eine Komponente ausgewählt werden.", 30 | "returnWithoutStart": "Ein return Knoten muss mit genau einen start Knoten verbunden sein. Es sind jetzt __inNodeLength__ start Knoten verbunden." 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /components/locales/de/component-start.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /components/locales/de/component-start.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Komponente", 6 | "name": "Name", 7 | "required": "erwartet", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Einzel" 13 | }, 14 | "outportChange": "Achtung", 15 | "componentInSubflow": "Nicht erlaubt in Subflows", 16 | "usecontext": "Use local context", 17 | "context_local": "local" 18 | }, 19 | "message": { 20 | "invalid_comp": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp' ist 'undefined' oder 'null'", 21 | "invalid_stack": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp.stack' ist 'undefined' oder 'null'", 22 | "invalid_idMatch": "Komponente '__nodeId__' empfing inkorrektes Event. ID stimmt nicht überein mit: __callerId__", 23 | "lastCaller": "letzter Aufruf von", 24 | "missingProperty": "Parameter '__parameter__' wird erwartet, hat aber keinen Wert.", 25 | "running": "läuft", 26 | "validationError": "Ungültiger Parameter '__parameter__'. Erwartet: '__expected__', Empfangen: '__invalidType__'. Wert: '__value__'.", 27 | "jsonValidationError": "Ungültiger Parameter '__parameter__'. Wert konnte nicht als JSON validiert werden: '__value__'.", 28 | "outportChange": "Die Output Ports haben sich geändert. Bitte diesen Dialog speichern, um die Anzahl und Bezeichnung der Ports zu aktualisieren.", 29 | "componentInSubflow": "Components dürfen nicht innerhalb von subflows definiert werden. Nur das Ausführen von Components ist erlaubt.", 30 | "componentNotConnected": "Für diesen Knoten muss eine Komponente ausgewählt werden.", 31 | "returnWithoutStart": "Ein return Knoten muss mit genau einen start Knoten verbunden sein. Es sind jetzt __inNodeLength__ start Knoten verbunden." 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /components/locales/de/run-component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/locales/de/run-component.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Komponente", 6 | "name": "Name", 7 | "required": "erwartet", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Einzel" 13 | }, 14 | "outportChange": "Achtung", 15 | "componentInSubflow": "Nicht erlaubt in Subflows" 16 | }, 17 | "message": { 18 | "invalid_comp": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp' ist 'undefined' oder 'null'", 19 | "invalid_stack": "Komponente '__nodeId__' empfing inkorrektes Event. 'msg._comp.stack' ist 'undefined' oder 'null'", 20 | "invalid_idMatch": "Komponente '__nodeId__' empfing inkorrektes Event. ID stimmt nicht überein mit: __callerId__", 21 | "lastCaller": "letzter Aufruf von", 22 | "missingProperty": "Parameter '__parameter__' wird erwartet, hat aber keinen Wert.", 23 | "running": "läuft", 24 | "hasValidationErrors": "Ungültige Parameter", 25 | "validationError": "Ungültiger Parameter '__parameter__'. Erwartet: '__expected__', Empfangen: '__invalidType__'. Wert: '__value__'.", 26 | "jsonValidationError": "Ungültiger Parameter '__parameter__'. Wert konnte nicht als JSON validiert werden: '__value__'.", 27 | "outportChange": "Die Output Ports haben sich geändert. Bitte diesen Dialog speichern, um die Anzahl und Bezeichnung der Ports zu aktualisieren.", 28 | "componentInSubflow": "Components dürfen nicht innerhalb von subflows definiert werden. Nur das Ausführen von Components ist erlaubt.", 29 | "componentNotConnected": "Für diesen Knoten muss eine Komponente ausgewählt werden.", 30 | "returnWithoutStart": "Ein return Knoten muss mit genau einen start Knoten verbunden sein. Es sind jetzt __inNodeLength__ start Knoten verbunden." 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /components/locales/en-US/component-return.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/locales/en-US/component-return.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Component", 6 | "name": "Name", 7 | "required": "required", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Separate" 13 | }, 14 | "outportChange": "Attention", 15 | "componentInSubflow": "Not allowed inside subflows", 16 | "global": "global" 17 | }, 18 | "message": { 19 | "invalid_comp": "component '__nodeId__' received invalid event. 'msg._comp' is 'undefined' or 'null'", 20 | "invalid_stack": "component '__nodeId__' received invalid event. 'msg._comp.stack' is 'undefined' or 'null'", 21 | "invalid_idMatch": "component '__nodeId__' received invalid event. ID does not match: __callerId__", 22 | "lastCaller": "last caller", 23 | "missingProperty": "component parameter '__parameter__' is required, but no value was found.", 24 | "running": "running", 25 | "validationError": "invalid parameter '__parameter__'. Expected type '__expected__', current type '__invalidType__'. Value is '__value__'.", 26 | "jsonValidationError": "invalid parameter '__parameter__': Value is not a valid JSON: '__value__'.", 27 | "outportChange": "Output ports have changed. Please save this Dialog to synchronize the number and labels.", 28 | "componentInSubflow": "Components must NOT be defined inside subflows. Executing Components is permitted.", 29 | "componentNotConnected": "This node has to be connected to a component.", 30 | "returnWithoutStart": "A component return node must have exactly one component start node. Found __inNodeLength__ start nodes" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /components/locales/en-US/component-start.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/locales/en-US/component-start.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Component", 6 | "name": "Name", 7 | "required": "required", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Separate" 13 | }, 14 | "outportChange": "Attention", 15 | "componentInSubflow": "Not allowed inside subflows", 16 | "usecontext": "Use local context", 17 | "context_local": "local" 18 | }, 19 | "message": { 20 | "invalid_comp": "component '__nodeId__' received invalid event. 'msg._comp' is 'undefined' or 'null'", 21 | "invalid_stack": "component '__nodeId__' received invalid event. 'msg._comp.stack' is 'undefined' or 'null'", 22 | "invalid_idMatch": "component '__nodeId__' received invalid event. ID does not match: __callerId__", 23 | "lastCaller": "last caller", 24 | "missingProperty": "component parameter '__parameter__' is required, but no value was found.", 25 | "running": "running", 26 | "validationError": "invalid parameter '__parameter__'. Expected type '__expected__', current type '__invalidType__'. Value is '__value__'.", 27 | "jsonValidationError": "invalid parameter '__parameter__': Value is not a valid JSON: '__value__'.", 28 | "outportChange": "Output ports have changed. Please save this Dialog to synchronize the number and labels.", 29 | "componentInSubflow": "Components must NOT be defined inside subflows. Executing Components is permitted.", 30 | "componentNotConnected": "This node has to be connected to a component.", 31 | "returnWithoutStart": "A component return node must have exactly one component start node. Found __inNodeLength__ start nodes" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /components/locales/en-US/run-component.html: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD:components/locales/en-US/components.html 2 | 25 | 26 | ======= 27 | >>>>>>> develop:components/locales/en-US/run-component.html 28 | 44 | <<<<<<< HEAD:components/locales/en-US/components.html 45 | 46 | 52 | ======= 53 | >>>>>>> develop:components/locales/en-US/run-component.html 54 | -------------------------------------------------------------------------------- /components/locales/en-US/run-component.json: -------------------------------------------------------------------------------- 1 | { 2 | "components": { 3 | "label": { 4 | "api": "API", 5 | "component": "Component", 6 | "name": "Name", 7 | "required": "required", 8 | "status": "Status", 9 | "mode": { 10 | "label": "Output Port", 11 | "default": "Default", 12 | "separate": "Separate" 13 | }, 14 | "outportChange": "Attention", 15 | "componentInSubflow": "Not allowed inside subflows" 16 | }, 17 | "message": { 18 | "invalid_comp": "component '__nodeId__' received invalid event. 'msg._comp' is 'undefined' or 'null'", 19 | "invalid_stack": "component '__nodeId__' received invalid event. 'msg._comp.stack' is 'undefined' or 'null'", 20 | "invalid_idMatch": "component '__nodeId__' received invalid event. ID does not match: __callerId__", 21 | "lastCaller": "last caller", 22 | "missingProperty": "component parameter '__parameter__' is required, but no value was found.", 23 | "running": "running", 24 | "hasValidationErrors": "Invalid Parameters", 25 | "validationError": "invalid parameter '__parameter__'. Expected type '__expected__', current type '__invalidType__'. Value is '__value__'.", 26 | "jsonValidationError": "invalid parameter '__parameter__': Value is not a valid JSON: '__value__'.", 27 | "outportChange": "Output ports have changed. Please save this Dialog to synchronize the number and labels.", 28 | "componentInSubflow": "Components must NOT be defined inside subflows. Executing Components is permitted.", 29 | "componentNotConnected": "This node has to be connected to a component.", 30 | "returnWithoutStart": "A component return node must have exactly one component start node. Found __inNodeLength__ start nodes" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /components/run-component.html: -------------------------------------------------------------------------------- 1 | 4 | 455 | -------------------------------------------------------------------------------- /components/run-component.js: -------------------------------------------------------------------------------- 1 | const componentsEmitter = require("./emitter"); 2 | 3 | module.exports = function (RED) { 4 | 5 | const EVENT_START_FLOW = "comp-start-flow"; 6 | const EVENT_RETURN_FLOW = "comp-flow-return"; 7 | 8 | function sendStartFlow(msg, node) { 9 | try { 10 | // create / update state for new execution 11 | if (typeof msg._comp == "undefined") { 12 | // create from scratch 13 | msg._comp = { 14 | stack: [] 15 | }; 16 | } 17 | 18 | // target node's id (component in) to start flow 19 | msg._comp.target = node.targetComponentId; 20 | // let targetComponent = RED.nodes.getNode(node.targetComponentId); 21 | let targetComponent = null; 22 | // work around for RED.nodes.getNode( not working in node-red-test-helper 23 | RED.nodes.eachNode((n) => { if (n.id == node.targetComponentId) { targetComponent = n } }); 24 | if (!targetComponent) { 25 | throw new Error("could not find node for id: " + node.targetComponentId); 26 | } 27 | let usecontext = targetComponent.usecontext; 28 | 29 | // push my ID onto the stack - the next return will come back to me 30 | let stackEntry = { callerId: node.id, targetId: node.targetComponentId } 31 | let context = {}; 32 | if (usecontext) { 33 | /* store the current context as parent. 34 | This works for global and local components. 35 | After this run node gets a return, it can restore this very current state in either case. 36 | Even, if there should be a msg.component on the root level, i.e. not created by a component, 37 | it will be finally conserved after all components return. 38 | */ 39 | if (msg.component) context._parent = msg.component; 40 | } else { 41 | // global flows store their current state as is. 42 | context = msg.component; 43 | } 44 | stackEntry.context = context; 45 | 46 | msg._comp.stack.push(stackEntry); 47 | 48 | // setup msg from parameters 49 | let validationErrors = {} 50 | // we read the list from the current targetComponent, as it might have changed without touching the RUN node. 51 | // That means, that some parameters are not reflected or changed in the paramSources. 52 | // That also means, that the RUN node should only keep track of: name, source, sourceType 53 | for (var paramIndex in targetComponent.api) { 54 | let paramDef = targetComponent.api[paramIndex]; 55 | let paramName = paramDef.name; 56 | let paramSource = node.paramSources[paramName]; 57 | let val = null; 58 | 59 | // If this RUN node has not yet a source definiton (was not touched after an API change), we just check for required params. 60 | if (!paramSource) { 61 | if (paramDef.required) { 62 | validationErrors[paramName] = "missing source. please set the parameter to a valid input" 63 | } 64 | continue; // more cheks not possible if we have no paramSource here. 65 | } 66 | 67 | // an empty, optional parameter is evaluated only, if the source type is "string". 68 | // In that case, the parameter is set(!). It is not put into the message in all other cases. 69 | if (paramSource.source && paramSource.source.length > 0 || paramSource.sourceType == "str") { 70 | val = RED.util.evaluateNodeProperty(paramSource.source, paramSource.sourceType, node, msg); 71 | } 72 | if (val == null || val == undefined) { 73 | if (paramDef.required) { 74 | validationErrors[paramName] = RED._("components.message.missingProperty", { parameter: paramName }); 75 | } 76 | } else { 77 | // validate types 78 | let type = typeof (val) 79 | switch (paramDef.type) { 80 | case "number": { 81 | if (type != "number") { 82 | validationErrors[paramName] = RED._("components.message.validationError", 83 | { parameter: paramName, expected: paramDef.type, invalidType: type, value: val }); 84 | } 85 | break; 86 | } 87 | case "string": { 88 | if (type != "string") { 89 | validationErrors[paramName] = RED._("components.message.validationError", 90 | { parameter: paramName, expected: paramDef.type, invalidType: type, value: val }); 91 | } 92 | break; 93 | } 94 | case "boolean": { 95 | if (type != "boolean") { 96 | validationErrors[paramName] = RED._("components.message.validationError", 97 | { parameter: paramName, expected: paramDef.type, invalidType: type, value: val }); 98 | } 99 | break; 100 | } 101 | case "json": { 102 | try { 103 | if (type != "object") { 104 | JSON.parse(val); 105 | } 106 | } catch (err) { 107 | validationErrors[paramName] = RED._("components.message.jsonValidationError", 108 | { parameter: paramName, value: val }); 109 | } 110 | break; 111 | } 112 | case "any": 113 | default: 114 | break; 115 | } 116 | } 117 | if (usecontext && paramDef.contextOption) { 118 | // local param 119 | context[paramName] = val; 120 | } else { 121 | msg[paramName] = val; 122 | } 123 | } 124 | 125 | if (usecontext) { 126 | msg.component = context; 127 | } 128 | 129 | if (Object.keys(validationErrors).length > 0) { 130 | node.status({ fill: "red", shape: "ring", text: RED._("components.message.hasValidationErrors") }); 131 | node.error({ validationErrors: validationErrors }); 132 | } else { 133 | // send event 134 | componentsEmitter.emit(EVENT_START_FLOW + "-" + node.targetComponentId, msg); 135 | } 136 | } catch (err) { 137 | console.trace(node.name || node.type, node.id, err); 138 | node.error(node.name || node.type, node.id, err); 139 | } 140 | } 141 | 142 | /* 143 | 144 | ******* RUN COMPONENT ************ 145 | second node: component (use a component) 146 | 147 | */ 148 | RED.nodes.registerType("component", component); 149 | function component(config) { 150 | // Create our node and event handler 151 | RED.nodes.createNode(this, config); 152 | 153 | var node = this; 154 | node.targetComponentId = config.targetComponentId || config.targetComponent.id; 155 | node.paramSources = config.paramSources; 156 | node.statuz = config.statuz; 157 | node.statuzType = config.statuzType; 158 | node.outLabels = config.outLabels; 159 | 160 | if (!node.targetComponentId) { 161 | node.error(RED._("components.message.componentNotConnected")) 162 | node.status({ fill: "red", shape: "dot", text: RED._("components.message.componentNotConnected") }) 163 | } 164 | 165 | function setStatuz(node, msg) { 166 | let done = (err, statuz) => { 167 | if (typeof (statuz) != "object") { 168 | statuz = { text: statuz } 169 | } 170 | node.status(statuz); 171 | } 172 | if (node.propertyType === 'jsonata') { 173 | RED.util.evaluateJSONataExpression(node.statuz, msg, (err, val) => { 174 | done(undefined, val) 175 | }); 176 | } else { 177 | let res = RED.util.evaluateNodeProperty(node.statuz, node.statuzType, node, msg, (err, val) => { 178 | done(undefined, val) 179 | }); 180 | } 181 | } 182 | 183 | var returnFromFlowHandler = function (msg) { 184 | if (msg._comp === undefined) { 185 | // this happens, if a receiver has already handled the event in this very same method. 186 | // Since the msg sent to all listeners is the same(!) js object and we modify it here, 187 | // the next listener does not receive the _comp anymore. And so should skip it, as there 188 | // can only be one legal receiver. 189 | return 190 | } 191 | if (typeof msg._comp.stack == "undefined" || msg._comp.stack == null || msg._comp.stack.length == 0) { 192 | node.error(RED._("components.message.invalid_stack", { nodeId: node.id }), msg); 193 | } 194 | let stack = msg._comp.stack 195 | let returnNode = msg._comp.returnNode; 196 | let myEntry = stack.slice(-1)[0]; 197 | let usecontext = myEntry.context ? true : false; 198 | let inOnlyScenario = !returnNode && myEntry.targetId == node.targetComponentId; 199 | let broadcastScenario = returnNode && returnNode.broadcast; 200 | let defaultScenario = returnNode && returnNode.callerId == config.id 201 | if (inOnlyScenario || broadcastScenario || defaultScenario) { 202 | // here, the message is for me 203 | stack.pop(); 204 | delete msg._comp.returnNode; 205 | if (stack.length == 0) { 206 | // stack is empty, so we are done. 207 | delete msg._comp; // -> following event listeners (component nodes) won't be able to handle this event. 208 | if (myEntry.context && myEntry.context._parent) { 209 | msg.component = myEntry.context._parent; 210 | } else { 211 | delete msg.component; // we are at the root, outside of components and there was no msg.component before. 212 | } 213 | } else { 214 | let parentEntry = stack.slice(-1)[0]; 215 | msg.component = parentEntry.context; 216 | } 217 | 218 | // find outport 219 | if (!returnNode || returnNode.mode == "default") { 220 | node.send(msg); 221 | } else { 222 | msgArr = []; 223 | let portLabel 224 | for (let i in node.outLabels) { 225 | portLabel = node.outLabels[i] 226 | if (portLabel == returnNode.name || portLabel == returnNode.id) { 227 | msgArr.push(msg); 228 | } else { 229 | msgArr.push(null); 230 | } 231 | } 232 | node.send(msgArr.length == 1 ? msg : msgArr); 233 | } 234 | setStatuz(node, msg); 235 | } 236 | } 237 | componentsEmitter.on(EVENT_RETURN_FLOW + "-" + node.id, returnFromFlowHandler); 238 | 239 | // Clean up event handler on close 240 | this.on("close", function () { 241 | componentsEmitter.removeListener(EVENT_RETURN_FLOW + "-" + node.id, returnFromFlowHandler); 242 | node.status({}); 243 | }); 244 | 245 | this.on("input", function (msg) { 246 | if (!node.targetComponentId) { 247 | node.error(RED._("components.message.componentNotConnected")) 248 | node.status({ fill: "red", shape: "dot", text: RED._("components.message.componentNotConnected") }) 249 | return 250 | } 251 | 252 | node.status({ fill: "green", shape: "ring", text: RED._("components.message.running") }); 253 | sendStartFlow(msg, node); 254 | }); 255 | 256 | } // END: RUN COMPONENT 257 | 258 | }; // end module.exports 259 | -------------------------------------------------------------------------------- /components/test/bugfix_missing_mode_prop_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var componentStart = require("../component-start.js"); 4 | var componentReturn = require("../component-return.js"); 5 | var runComponent = require("../run-component.js"); 6 | const { startServer } = require("node-red-node-test-helper"); 7 | 8 | helper.init(require.resolve('node-red')); 9 | 10 | var testFlow = [ 11 | { 12 | id: "run01", 13 | "type": "component", 14 | "name": "run 01", 15 | "targetComponent": { 16 | "id": "in01", 17 | "name": "in 01", 18 | }, 19 | "paramSources": {}, 20 | "statuz": "", 21 | "statuzType": "str", 22 | "outputs": 1, 23 | "outLabels": [ 24 | "default" 25 | ], 26 | "wires": [ 27 | [ 28 | "debug01" 29 | ] 30 | ] 31 | }, 32 | { 33 | "id": "in01", 34 | "type": "component_in", 35 | "name": "in 01", 36 | "api": [], 37 | "wires": [ 38 | [ 39 | "ret01" 40 | ] 41 | ] 42 | }, 43 | { 44 | "id": "ret01", 45 | "type": "component_out", 46 | "name": "ret 01a", 47 | "mode": undefined, 48 | "wires": [] 49 | }, 50 | { id: "debug01", type: "helper" } 51 | ] 52 | 53 | describe('legacy return nodes with mode prop undefined', function () { 54 | 55 | before(function (done) { 56 | helper.startServer(done); 57 | }); 58 | 59 | after(function (done) { 60 | helper.stopServer(done); 61 | }); 62 | 63 | afterEach(function () { 64 | helper.unload(); 65 | }); 66 | 67 | it('should return messages', function (done) { 68 | helper.load([componentStart, componentReturn, runComponent], testFlow, {}, function () { 69 | var debug01 = helper.getNode("debug01"); 70 | debug01.on("input", function (msg) { 71 | try { 72 | msg.should.have.property("payload", "Works!") 73 | done(); 74 | } catch (e) { 75 | done(e); 76 | } 77 | }); 78 | var run01 = helper.getNode("run01"); 79 | run01.receive({ 80 | payload: "Works!" 81 | }); 82 | }); 83 | }); 84 | }); -------------------------------------------------------------------------------- /components/test/multiple_out_ports_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var componentStart = require("../component-start.js"); 4 | var componentReturn = require("../component-return.js"); 5 | var runComponent = require("../run-component.js"); 6 | 7 | helper.init(require.resolve('node-red')); 8 | 9 | var testFlow = [ 10 | { 11 | "id": "c58cbb4f.4a7a98", 12 | "type": "component_in", 13 | "name": "Component 4", 14 | "api": [], 15 | "wires": [ 16 | [ 17 | "6b50b048.8fd3e", 18 | "30c7b937.ca1356", 19 | "3ba50700.4d49fa", 20 | "13436b52.301765" 21 | ] 22 | ] 23 | }, 24 | { 25 | "id": "run01", 26 | "type": "component", 27 | "name": "run 04", 28 | "targetComponent": { 29 | "id": "c58cbb4f.4a7a98", 30 | "name": "Component 4", 31 | "api": [] 32 | }, 33 | "paramSources": {}, 34 | "statuz": "", 35 | "statuzType": "str", 36 | "outputs": 3, 37 | "outLabels": [ 38 | "default", 39 | "ret 04.3", 40 | "ret 04.4" 41 | ], 42 | "wires": [ 43 | [ 44 | "debug01" 45 | ], 46 | [ 47 | "debug02" 48 | ], 49 | [ 50 | "debug03" 51 | ] 52 | ] 53 | }, 54 | { 55 | "id": "6b50b048.8fd3e", 56 | "type": "component_out", 57 | "name": "ret 04.1", 58 | "mode": "default", 59 | "wires": [] 60 | }, 61 | { 62 | "id": "30c7b937.ca1356", 63 | "type": "component_out", 64 | "name": "ret 04.2", 65 | "mode": "default", 66 | "wires": [] 67 | }, 68 | { 69 | "id": "3ba50700.4d49fa", 70 | "type": "component_out", 71 | "name": "ret 04.3", 72 | "mode": "separate", 73 | "wires": [] 74 | }, 75 | { 76 | "id": "13436b52.301765", 77 | "type": "component_out", 78 | "name": "ret 04.4", 79 | "mode": "separate", 80 | "wires": [] 81 | }, 82 | { id: "debug01", type: "helper" }, 83 | { id: "debug02", type: "helper" }, 84 | { id: "debug03", type: "helper" }, 85 | { id: "debug04", type: "helper" }, 86 | ] 87 | 88 | describe('multiple return nodes', function () { 89 | 90 | before(function (done) { 91 | helper.startServer(done); 92 | }); 93 | 94 | after(function (done) { 95 | helper.stopServer(done); 96 | }); 97 | 98 | afterEach(function () { 99 | helper.unload(); 100 | }); 101 | 102 | describe('with mixed modes', function () { 103 | 104 | it('should send messages to the correct ports', function (done) { 105 | helper.load([componentStart, componentReturn, runComponent], testFlow, {}, function () { 106 | 107 | var count01 = 0; 108 | 109 | var debug01 = helper.getNode("debug01"); 110 | debug01.on("input", function (msg) { 111 | try { 112 | msg.should.have.property("payload", "Works!") 113 | count01++ 114 | if (count01 == 1) { 115 | done() 116 | } 117 | } catch (e) { 118 | done(e); 119 | } 120 | }); 121 | var debug02 = helper.getNode("debug02"); 122 | debug02.on("input", function (msg) { 123 | msg.should.have.property("payload", "Works!") 124 | //console.log("debug 02", count02++, msg) 125 | }); 126 | var debug03 = helper.getNode("debug03"); 127 | debug03.on("input", function (msg) { 128 | console.log("debug 03", count03++, msg) 129 | }); 130 | 131 | var run01 = helper.getNode("run01"); 132 | run01.receive({ 133 | payload: "Works!" 134 | }); 135 | 136 | 137 | }); 138 | }); 139 | }); 140 | 141 | describe('all set to default', function () { 142 | 143 | before(function (done) { 144 | testFlow.forEach((node) => { 145 | if (node.name == "ret 04.3" || node.name == "ret 04.4") { 146 | node.mode = "default"; 147 | } 148 | }) 149 | done() 150 | }); 151 | 152 | it('should send messages to only the default ports', function (done) { 153 | helper.load([componentStart, componentReturn, runComponent], testFlow, {}, function () { 154 | 155 | var count01 = 0; 156 | 157 | var debug01 = helper.getNode("debug01"); 158 | debug01.on("input", function (msg) { 159 | try { 160 | msg.should.have.property("payload", "Works!") 161 | count01++ 162 | if (count01 == 3) { 163 | done() 164 | } 165 | } catch (e) { 166 | done(e); 167 | } 168 | }); 169 | var debug02 = helper.getNode("debug02"); 170 | debug02.on("input", function (msg) { 171 | done(new Error("should not succeed")); 172 | }); 173 | var debug03 = helper.getNode("debug03"); 174 | debug03.on("input", function (msg) { 175 | done(new Error("should not succeed")); 176 | }); 177 | 178 | var run01 = helper.getNode("run01"); 179 | run01.receive({ 180 | payload: "Works!" 181 | }); 182 | 183 | 184 | }); 185 | }); 186 | }); 187 | 188 | describe('all set to separate', function () { 189 | 190 | before(function (done) { 191 | testFlow.forEach((node) => { 192 | switch (node.name) { 193 | case "ret 04.1": 194 | case "ret 04.2": 195 | case "ret 04.3": 196 | case "ret 04.4": { 197 | node.mode = "separate"; 198 | } 199 | case "run 04": { 200 | node.outputs = 4; 201 | node.outLabels = [ 202 | "ret 04.1", 203 | "ret 04.2", 204 | "ret 04.3", 205 | "ret 04.4" 206 | ]; 207 | node.wires = [ 208 | [ 209 | "debug01" 210 | ], 211 | [ 212 | "debug02" 213 | ], 214 | [ 215 | "debug03" 216 | ], 217 | [ 218 | "debug04" 219 | ] 220 | ] 221 | 222 | } 223 | } 224 | }) 225 | done() 226 | }); 227 | 228 | it('should send messages to the matching ports', function (done) { 229 | helper.load([componentStart, componentReturn, runComponent], testFlow, {}, function () { 230 | 231 | var debug01 = helper.getNode("debug01"); 232 | debug01.on("input", function (msg) { 233 | try { 234 | msg.should.have.property("payload", "Works!") 235 | } catch (e) { 236 | done(e); 237 | } 238 | }); 239 | var debug02 = helper.getNode("debug02"); 240 | debug02.on("input", function (msg) { 241 | try { 242 | msg.should.have.property("payload", "Works!") 243 | } catch (e) { 244 | done(e); 245 | } 246 | }); 247 | var debug03 = helper.getNode("debug03"); 248 | debug03.on("input", function (msg) { 249 | try { 250 | msg.should.have.property("payload", "Works!") 251 | } catch (e) { 252 | done(e); 253 | } 254 | }); 255 | var debug04 = helper.getNode("debug04"); 256 | debug04.on("input", function (msg) { 257 | try { 258 | msg.should.have.property("payload", "Works!") 259 | done(); 260 | } catch (e) { 261 | done(e); 262 | } 263 | }); 264 | 265 | var run01 = helper.getNode("run01"); 266 | run01.receive({ 267 | payload: "Works!" 268 | }); 269 | 270 | 271 | }); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /components/test/nested_local_global_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var changeNode = require("@node-red/nodes/core/function/15-change"); 4 | var componentStart = require("../component-start.js"); 5 | var componentReturn = require("../component-return.js"); 6 | var runComponent = require("../run-component.js"); 7 | 8 | helper.init(require.resolve('node-red')); 9 | 10 | const testFlow1 = [ 11 | { 12 | "id": "tab", 13 | "type": "tab" 14 | }, 15 | { 16 | "id": "IN outer local", 17 | "type": "component_in", 18 | "z": "tab", 19 | "name": "outer local", 20 | "api": [ 21 | { 22 | "name": "prop", 23 | "type": "json", 24 | "required": false, 25 | "contextOption": true 26 | } 27 | ], 28 | "usecontext": true, 29 | "wires": [ 30 | [ 31 | "change 2", 32 | "after IN outer local" 33 | ] 34 | ] 35 | }, 36 | { 37 | "id": "IN inner global", 38 | "type": "component_in", 39 | "z": "tab", 40 | "name": "inner global", 41 | "api": [ 42 | { 43 | "name": "prop", 44 | "type": "json", 45 | "required": true, 46 | "contextOption": true 47 | } 48 | ], 49 | "usecontext": false, 50 | "wires": [ 51 | [ 52 | "change 01" 53 | ] 54 | ] 55 | }, 56 | { 57 | "id": "OUT 01", 58 | "type": "component_out", 59 | "z": "tab", 60 | "name": null, 61 | "mode": "default", 62 | "component_definitions_are_NOT_allowed_inside_subflows": false, 63 | "wires": [] 64 | }, 65 | { 66 | "id": "OUT 02", 67 | "type": "component_out", 68 | "z": "tab", 69 | "name": null, 70 | "mode": "default", 71 | "component_definitions_are_NOT_allowed_inside_subflows": false, 72 | "wires": [] 73 | }, 74 | { 75 | "id": "RUN inner global", 76 | "type": "component", 77 | "z": "tab", 78 | "name": "", 79 | "targetComponent": null, 80 | "targetComponentId": "IN inner global", 81 | "paramSources": { 82 | "prop": { 83 | "name": "prop", 84 | "source": "{\"Outer\": \"string\"}", 85 | "sourceType": "json" 86 | } 87 | }, 88 | "statuz": "", 89 | "statuzType": "str", 90 | "outputs": 1, 91 | "outLabels": [ 92 | "default" 93 | ], 94 | "wires": [ 95 | [ 96 | "OUT 01", 97 | "after RUN inner global" 98 | ] 99 | ] 100 | }, 101 | { 102 | "id": "change 01", 103 | "type": "change", 104 | "z": "tab", 105 | "name": "", 106 | "rules": [ 107 | { 108 | "t": "set", 109 | "p": "inner", 110 | "pt": "msg", 111 | "to": "42", 112 | "tot": "num" 113 | }, 114 | { 115 | "t": "set", 116 | "p": "component.prop.test", 117 | "pt": "msg", 118 | "to": "101", 119 | "tot": "num" 120 | } 121 | ], 122 | "action": "", 123 | "property": "", 124 | "from": "", 125 | "to": "", 126 | "reg": false, 127 | "wires": [ 128 | [ 129 | "after change 01", 130 | "RUN deep local" 131 | ] 132 | ] 133 | }, 134 | { 135 | "id": "RUN outer local", 136 | "type": "component", 137 | "z": "tab", 138 | "name": "outer local", 139 | "targetComponent": null, 140 | "targetComponentId": "IN outer local", 141 | "paramSources": { 142 | "prop": { 143 | "name": "prop", 144 | "source": "{}", 145 | "sourceType": "json" 146 | } 147 | }, 148 | "statuz": "", 149 | "statuzType": "str", 150 | "outputs": 1, 151 | "outLabels": [ 152 | "default" 153 | ], 154 | "wires": [ 155 | [ 156 | "after RUN outer local" 157 | ] 158 | ] 159 | }, 160 | { 161 | "id": "after change 01", 162 | "type": "helper", 163 | "z": "tab", 164 | }, 165 | { 166 | "id": "after RUN inner global", 167 | "type": "helper", 168 | "z": "tab", 169 | }, 170 | { 171 | "id": "after RUN outer local", 172 | "type": "helper", 173 | "z": "tab", 174 | }, 175 | { 176 | "id": "IN deep local", 177 | "type": "component_in", 178 | "z": "tab", 179 | "name": "deep local", 180 | "api": [ 181 | { 182 | "name": "propDeep", 183 | "type": "string", 184 | "required": false, 185 | "contextOption": true 186 | } 187 | ], 188 | "usecontext": true, 189 | "wires": [ 190 | [ 191 | "OUT 03", 192 | "after IN deep local" 193 | ] 194 | ] 195 | }, 196 | { 197 | "id": "OUT 03", 198 | "type": "component_out", 199 | "z": "tab", 200 | "name": null, 201 | "mode": "default", 202 | "component_definitions_are_NOT_allowed_inside_subflows": false, 203 | "wires": [] 204 | }, 205 | { 206 | "id": "after IN deep local", 207 | "type": "helper", 208 | "z": "tab", 209 | }, 210 | { 211 | "id": "RUN deep local", 212 | "type": "component", 213 | "z": "tab", 214 | "name": "", 215 | "targetComponent": null, 216 | "targetComponentId": "IN deep local", 217 | "paramSources": { 218 | "propDeep": { 219 | "name": "propDeep", 220 | "source": "Moin", 221 | "sourceType": "str" 222 | } 223 | }, 224 | "statuz": "", 225 | "statuzType": "str", 226 | "outputs": 1, 227 | "outLabels": [ 228 | "default" 229 | ], 230 | "wires": [ 231 | [ 232 | "OUT 02", 233 | "after RUN deep local" 234 | ] 235 | ] 236 | }, 237 | { 238 | "id": "after RUN deep local", 239 | "type": "helper", 240 | "z": "tab", 241 | }, 242 | { 243 | "id": "change 2", 244 | "type": "change", 245 | "z": "tab", 246 | "name": "", 247 | "rules": [ 248 | { 249 | "t": "set", 250 | "p": "component.prop.test", 251 | "pt": "msg", 252 | "to": "099", 253 | "tot": "num" 254 | } 255 | ], 256 | "action": "", 257 | "property": "", 258 | "from": "", 259 | "to": "", 260 | "reg": false, 261 | "wires": [ 262 | [ 263 | "RUN inner global" 264 | ] 265 | ] 266 | }, 267 | { 268 | "id": "after IN outer local", 269 | "type": "helper", 270 | "z": "tab", 271 | } 272 | ] 273 | 274 | 275 | describe('nested components global and local', function () { 276 | 277 | before(function (done) { 278 | helper.startServer(done); 279 | }); 280 | 281 | after(function (done) { 282 | helper.stopServer(done); 283 | }); 284 | 285 | afterEach(function () { 286 | helper.unload(); 287 | }); 288 | 289 | it.only('should basically work', function (done) { 290 | helper.load([componentStart, componentReturn, runComponent, changeNode], testFlow1, {}, function () { 291 | var debug01 = helper.getNode("after RUN outer local"); 292 | debug01.on("input", function (msg) { 293 | try { 294 | msg.should.have.property("prop").which.has.property("Outer").which.equals("string"); 295 | msg.should.have.property("inner").which.equals(42); 296 | done(); 297 | } catch (e) { 298 | done(e); 299 | } 300 | }); 301 | var debug02 = helper.getNode("after IN outer local"); 302 | debug02.on("input", function (msg) { 303 | try { 304 | msg.should.have.property("component").which.eql({ prop: {} }); 305 | } catch (e) { 306 | done(e); 307 | } 308 | }); 309 | var debug03 = helper.getNode("after change 01"); 310 | debug03.on("input", function (msg) { 311 | try { 312 | msg.should.have.property("component").which.eql({ prop: { test: 101 } }); 313 | msg.should.have.property("inner").which.eql(42); 314 | } catch (e) { 315 | done(e); 316 | } 317 | }); 318 | var debug04 = helper.getNode("after IN deep local"); 319 | debug04.on("input", function (msg) { 320 | try { 321 | msg.should.have.property("component").which.eql({ propDeep: "Moin", _parent: { prop: { test: 101 } } }); 322 | msg.should.have.property("prop").which.eql({ Outer: "string" }); 323 | msg.should.have.property("inner").which.eql(42); 324 | } catch (e) { 325 | done(e); 326 | } 327 | }); 328 | var debug05 = helper.getNode("after RUN deep local"); 329 | debug05.on("input", function (msg) { 330 | try { 331 | msg.should.have.property("component").which.eql({ prop: { test: 101 } }); 332 | msg.should.have.property("prop").which.eql({ Outer: "string" }); 333 | msg.should.have.property("inner").which.eql(42); 334 | } catch (e) { 335 | done(e); 336 | } 337 | }); 338 | var debug06 = helper.getNode("after RUN inner global"); 339 | debug06.on("input", function (msg) { 340 | try { 341 | msg.should.have.property("component").which.eql({ prop: { test: 101 } }); 342 | msg.should.have.property("prop").which.eql({ Outer: "string" }); 343 | msg.should.have.property("inner").which.eql(42); 344 | } catch (e) { 345 | done(e); 346 | } 347 | }); 348 | var run = helper.getNode("RUN outer local"); 349 | run.receive({ 350 | payload: { 351 | "data": true 352 | } 353 | }); 354 | }); 355 | }); 356 | }); -------------------------------------------------------------------------------- /components/test/nested_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var changeNode = require("@node-red/nodes/core/function/15-change"); 4 | var linkNode = require("@node-red/nodes/core/common/60-link"); 5 | var catchNode = require("@node-red/nodes/core/common/25-catch"); 6 | var componentStart = require("../component-start.js"); 7 | var componentReturn = require("../component-return.js"); 8 | var runComponent = require("../run-component.js"); 9 | 10 | helper.init(require.resolve('node-red')); 11 | 12 | var testFlow = [ 13 | { 14 | id: "tab", 15 | type: "tab", 16 | label: "Test flow" 17 | }, 18 | { 19 | id: "run01", 20 | "type": "component", 21 | "z": "tab", 22 | "name": "run 01", 23 | "targetComponent": { 24 | "id": "in01", 25 | "name": "in 01", 26 | "api": [ 27 | { 28 | "name": "name1", 29 | "type": "string", 30 | "required": true 31 | }, 32 | { 33 | "name": "Das ist ein längerer", 34 | "type": "json", 35 | "required": true 36 | } 37 | ] 38 | }, 39 | "paramSources": { 40 | "name1": { 41 | "name": "name1", 42 | "type": "string", 43 | "required": true, 44 | "source": "\"Test\"", 45 | "sourceType": "jsonata" 46 | }, 47 | "Das ist ein längerer": { 48 | "name": "Das ist ein längerer", 49 | "type": "json", 50 | "required": true, 51 | "source": "payload.inner[\"even more\"]", 52 | "sourceType": "msg" 53 | } 54 | }, 55 | "statuz": "name1", 56 | "statuzType": "msg", 57 | "outputs": 2, 58 | "outLabels": [ 59 | "default", 60 | "ret 01b" 61 | ], 62 | "wires": [ 63 | [ 64 | "debug01" 65 | ], 66 | [ 67 | "debug02" 68 | ] 69 | ] 70 | }, 71 | { 72 | "id": "in01", 73 | "type": "component_in", 74 | "z": "tab", 75 | "name": "in 01", 76 | "api": [ 77 | { 78 | "name": "name1", 79 | "type": "string", 80 | "required": true 81 | }, 82 | { 83 | "name": "Das ist ein längerer", 84 | "type": "json", 85 | "required": true 86 | } 87 | ], 88 | "wires": [ 89 | [ 90 | "change01" 91 | ] 92 | ] 93 | }, 94 | { 95 | "id": "change01", 96 | "type": "change", 97 | "z": "tab", 98 | "name": "", 99 | "rules": [ 100 | { 101 | "t": "set", 102 | "p": "outer", 103 | "pt": "msg", 104 | "to": "{\"test\": 42}", 105 | "tot": "json" 106 | } 107 | ], 108 | "action": "", 109 | "property": "", 110 | "from": "", 111 | "to": "", 112 | "reg": false, 113 | "wires": [ 114 | [ 115 | "run02", 116 | "out01b" 117 | ] 118 | ] 119 | }, 120 | { 121 | "id": "run02", 122 | "type": "component", 123 | "z": "tab", 124 | "name": "run 02", 125 | "targetComponent": { 126 | "id": "in02", 127 | "name": "out 02", 128 | "api": [] 129 | }, 130 | "paramSources": {}, 131 | "statuz": "", 132 | "statuzType": "str", 133 | "outputs": 1, 134 | "outLabels": [ 135 | "ret 02" 136 | ], 137 | "wires": [ 138 | [ 139 | "out01a" 140 | ] 141 | ] 142 | }, 143 | { 144 | "id": "out01a", 145 | "type": "component_out", 146 | "z": "tab", 147 | "name": "ret 01a", 148 | "mode": "default", 149 | "wires": [] 150 | }, 151 | { 152 | "id": "out01b", 153 | "type": "component_out", 154 | "z": "tab", 155 | "name": "ret 01b", 156 | "mode": "separate", 157 | "wires": [] 158 | }, 159 | { 160 | "id": "in02", 161 | "type": "component_in", 162 | "z": "tab", 163 | "name": "in 02", 164 | "api": [], 165 | "wires": [ 166 | [ 167 | "change02" 168 | ] 169 | ] 170 | }, 171 | { 172 | "id": "change02", 173 | "type": "change", 174 | "z": "tab", 175 | "name": "", 176 | "rules": [ 177 | { 178 | "t": "set", 179 | "p": "inner", 180 | "pt": "msg", 181 | "to": "23", 182 | "tot": "num" 183 | } 184 | ], 185 | "action": "", 186 | "property": "", 187 | "from": "", 188 | "to": "", 189 | "reg": false, 190 | "wires": [ 191 | [ 192 | "linkOut01" 193 | ] 194 | ] 195 | }, 196 | { 197 | "id": "linkOut01", 198 | "type": "link out", 199 | "z": "tab", 200 | "name": "link out 01", 201 | "links": [ 202 | "linkIn01" 203 | ], 204 | "wires": [] 205 | }, 206 | { 207 | "id": "linkIn01", 208 | "type": "link in", 209 | "z": "tab", 210 | "name": "link in 01", 211 | "links": [ 212 | "linkOut01" 213 | ], 214 | "x": 435, 215 | "y": 440, 216 | "wires": [ 217 | [ 218 | "out02" 219 | ] 220 | ] 221 | }, 222 | { 223 | "id": "out02", 224 | "type": "component_out", 225 | "z": "tab", 226 | "name": "ret 02", 227 | "mode": "separate", 228 | "wires": [] 229 | }, 230 | { id: "debug01", type: "helper" }, 231 | { id: "debug02", type: "helper" } 232 | ] 233 | 234 | describe('nested components, connected by links', function () { 235 | 236 | before(function (done) { 237 | helper.startServer(done); 238 | }); 239 | 240 | after(function (done) { 241 | helper.stopServer(done); 242 | }); 243 | 244 | afterEach(function () { 245 | helper.unload(); 246 | }); 247 | 248 | it('should basically work', function (done) { 249 | helper.load([componentStart, componentReturn, runComponent, changeNode, linkNode, catchNode], testFlow, {}, function () { 250 | var debug01 = helper.getNode("debug01"); 251 | debug01.on("input", function (msg) { 252 | try { 253 | msg.should.have.property("inner") 254 | } catch (e) { 255 | done(e); 256 | } 257 | }); 258 | var debug02 = helper.getNode("debug02"); 259 | debug02.on("input", function (msg) { 260 | try { 261 | msg.should.have.property("outer") 262 | done(); 263 | } catch (e) { 264 | done(e); 265 | } 266 | }); 267 | var run01 = helper.getNode("run01"); 268 | run01.receive({ 269 | payload: { 270 | inner: { 271 | "even more": 999 272 | } 273 | } 274 | }); 275 | }); 276 | }); 277 | }); -------------------------------------------------------------------------------- /components/test/param_validation_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var componentStart = require("../component-start.js"); 4 | var componentReturn = require("../component-return.js"); 5 | var runComponent = require("../run-component.js"); 6 | 7 | helper.init(require.resolve('node-red')); 8 | 9 | var flowValidations = [ 10 | { 11 | "id": "01", 12 | "type": "component_in", 13 | "name": "validations", 14 | "api": [ 15 | { 16 | "name": "string", 17 | "type": "string", 18 | "required": false, 19 | "contextOption": false 20 | }, 21 | { 22 | "name": "number", 23 | "type": "number", 24 | "required": false, 25 | "contextOption": false 26 | }, 27 | { 28 | "name": "json", 29 | "type": "json", 30 | "required": false, 31 | "contextOption": false 32 | }, 33 | { 34 | "name": "boolean", 35 | "type": "boolean", 36 | "required": false, 37 | "contextOption": false 38 | }, 39 | { 40 | "name": "any", 41 | "type": "any", 42 | "required": false, 43 | "contextOption": false 44 | }, 45 | { 46 | "name": "req_string", 47 | "type": "string", 48 | "required": true, 49 | "contextOption": false 50 | }, 51 | { 52 | "name": "req_number", 53 | "type": "number", 54 | "required": true, 55 | "contextOption": false 56 | }, 57 | { 58 | "name": "req_json", 59 | "type": "json", 60 | "required": true, 61 | "contextOption": false 62 | }, 63 | { 64 | "name": "req_boolean", 65 | "type": "boolean", 66 | "required": true, 67 | "contextOption": false 68 | }, 69 | { 70 | "name": "req_any", 71 | "type": "any", 72 | "required": true, 73 | "contextOption": false 74 | } 75 | ], 76 | "usecontext": false, 77 | "wires": [ 78 | [ 79 | "02" 80 | ] 81 | ] 82 | }, 83 | { 84 | "id": "02", 85 | "type": "component_out", 86 | "mode": "default", 87 | "wires": [] 88 | }, 89 | { 90 | "id": "03", 91 | "type": "component", 92 | "targetComponentId": "01", 93 | "paramSources": { 94 | "string": { 95 | "name": "string", 96 | "source": "payload", 97 | "sourceType": "msg" 98 | }, 99 | "number": { 100 | "name": "number", 101 | "source": "payload", 102 | "sourceType": "msg" 103 | }, 104 | "json": { 105 | "name": "json", 106 | "source": "payload", 107 | "sourceType": "msg" 108 | }, 109 | "boolean": { 110 | "name": "boolean", 111 | "source": "payload", 112 | "sourceType": "msg" 113 | }, 114 | "any": { 115 | "name": "any", 116 | "source": "payload", 117 | "sourceType": "msg" 118 | }, 119 | "req_string": { 120 | "name": "req_string", 121 | "source": "payload", 122 | "sourceType": "msg" 123 | }, 124 | "req_number": { 125 | "name": "req_number", 126 | "source": "payload", 127 | "sourceType": "msg" 128 | }, 129 | "req_json": { 130 | "name": "req_json", 131 | "source": "payload", 132 | "sourceType": "msg" 133 | }, 134 | "req_boolean": { 135 | "name": "req_boolean", 136 | "source": "payload", 137 | "sourceType": "msg" 138 | }, 139 | "req_any": { 140 | "name": "req_any", 141 | "source": "payload", 142 | "sourceType": "msg" 143 | } 144 | }, 145 | "statuz": "", 146 | "statuzType": "str", 147 | "outputs": 1, 148 | "outLabels": [ 149 | "default" 150 | ], 151 | "wires": [ 152 | [ 153 | "debug" 154 | ] 155 | ] 156 | }, 157 | { 158 | "id": "04", 159 | "type": "component", 160 | "targetComponentId": "01", 161 | "paramSources": { 162 | "string": { 163 | "name": "string", 164 | "source": "none", 165 | "sourceType": "msg" 166 | }, 167 | "number": { 168 | "name": "number", 169 | "source": "none", 170 | "sourceType": "msg" 171 | }, 172 | "json": { 173 | "name": "json", 174 | "source": "none", 175 | "sourceType": "msg" 176 | }, 177 | "boolean": { 178 | "name": "boolean", 179 | "source": "none", 180 | "sourceType": "msg" 181 | }, 182 | "any": { 183 | "name": "any", 184 | "source": "none", 185 | "sourceType": "msg" 186 | }, 187 | "req_string": { 188 | "name": "req_string", 189 | "source": "none", 190 | "sourceType": "msg" 191 | }, 192 | "req_number": { 193 | "name": "req_number", 194 | "source": "none", 195 | "sourceType": "msg" 196 | }, 197 | "req_json": { 198 | "name": "req_json", 199 | "source": "none", 200 | "sourceType": "msg" 201 | }, 202 | "req_boolean": { 203 | "name": "req_boolean", 204 | "source": "none", 205 | "sourceType": "msg" 206 | }, 207 | "req_any": { 208 | "name": "req_any", 209 | "source": "none", 210 | "sourceType": "msg" 211 | } 212 | }, 213 | "statuz": "", 214 | "statuzType": "str", 215 | "outputs": 1, 216 | "outLabels": [ 217 | "default" 218 | ], 219 | "wires": [ 220 | [ 221 | "debug" 222 | ] 223 | ] 224 | }, 225 | { 226 | "id": "debug", 227 | "type": "helper", 228 | } 229 | ] 230 | 231 | describe('parameters with wrong type', function () { 232 | 233 | before(function (done) { 234 | helper.startServer(done); 235 | }); 236 | 237 | after(function (done) { 238 | helper.stopServer(done); 239 | }); 240 | 241 | afterEach(function () { 242 | helper.unload(); 243 | }); 244 | 245 | it('(string) should add a validation error', function (done) { 246 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 247 | var run = helper.getNode("03"); 248 | run.on("input", function (msg) { 249 | try { 250 | let expectedErrors = { 251 | validationErrors: { 252 | "number": "components.message.validationError", 253 | "json": "components.message.jsonValidationError", 254 | "boolean": "components.message.validationError", 255 | "req_number": "components.message.validationError", 256 | "req_json": "components.message.jsonValidationError", 257 | "req_boolean": "components.message.validationError" 258 | } 259 | }; 260 | run.error.should.be.calledWithExactly(expectedErrors); 261 | done(); 262 | } catch (e) { 263 | done(e); 264 | } 265 | }); 266 | run.receive({ 267 | payload: "test" 268 | }); 269 | }); 270 | }); 271 | 272 | it('(number) should add a validation error', function (done) { 273 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 274 | var run = helper.getNode("03"); 275 | run.on("input", function (msg) { 276 | try { 277 | let expectedErrors = { 278 | validationErrors: { 279 | "string": "components.message.validationError", 280 | "boolean": "components.message.validationError", 281 | "req_string": "components.message.validationError", 282 | "req_boolean": "components.message.validationError" 283 | } 284 | }; 285 | run.error.should.be.calledWithExactly(expectedErrors); 286 | done(); 287 | } catch (e) { 288 | done(e); 289 | } 290 | }); 291 | run.receive({ 292 | payload: 42 293 | }); 294 | }); 295 | }); 296 | 297 | it('(boolean) should add a validation error', function (done) { 298 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 299 | var run = helper.getNode("03"); 300 | run.on("input", function (msg) { 301 | try { 302 | let expectedErrors = { 303 | validationErrors: { 304 | "string": "components.message.validationError", 305 | "number": "components.message.validationError", 306 | "req_string": "components.message.validationError", 307 | "req_number": "components.message.validationError", 308 | } 309 | }; 310 | run.error.should.be.calledWithExactly(expectedErrors); 311 | done(); 312 | } catch (e) { 313 | done(e); 314 | } 315 | }); 316 | run.receive({ 317 | payload: true 318 | }); 319 | }); 320 | }); 321 | 322 | it('(array) should add a validation error', function (done) { 323 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 324 | var run = helper.getNode("03"); 325 | run.on("input", function (msg) { 326 | try { 327 | let expectedErrors = { 328 | validationErrors: { 329 | "string": "components.message.validationError", 330 | "number": "components.message.validationError", 331 | "boolean": "components.message.validationError", 332 | "req_string": "components.message.validationError", 333 | "req_number": "components.message.validationError", 334 | "req_boolean": "components.message.validationError" 335 | } 336 | }; 337 | run.error.should.be.calledWithExactly(expectedErrors); 338 | done(); 339 | } catch (e) { 340 | done(e); 341 | } 342 | }); 343 | run.receive({ 344 | payload: [] 345 | }); 346 | }); 347 | }); 348 | 349 | it('(object) should add a validation error', function (done) { 350 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 351 | var run = helper.getNode("03"); 352 | run.on("input", function (msg) { 353 | try { 354 | let expectedErrors = { 355 | validationErrors: { 356 | "string": "components.message.validationError", 357 | "number": "components.message.validationError", 358 | "boolean": "components.message.validationError", 359 | "req_string": "components.message.validationError", 360 | "req_number": "components.message.validationError", 361 | "req_boolean": "components.message.validationError" 362 | } 363 | }; 364 | run.error.should.be.calledWithExactly(expectedErrors); 365 | done(); 366 | } catch (e) { 367 | done(e); 368 | } 369 | }); 370 | run.receive({ 371 | payload: {} 372 | }); 373 | }); 374 | }); 375 | 376 | it('(json string) should add a validation error', function (done) { 377 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 378 | var run = helper.getNode("03"); 379 | run.on("input", function (msg) { 380 | try { 381 | let expectedErrors = { 382 | validationErrors: { 383 | "number": "components.message.validationError", 384 | "boolean": "components.message.validationError", 385 | "req_number": "components.message.validationError", 386 | "req_boolean": "components.message.validationError" 387 | } 388 | }; 389 | run.error.should.be.calledWithExactly(expectedErrors); 390 | done(); 391 | } catch (e) { 392 | done(e); 393 | } 394 | }); 395 | run.receive({ 396 | payload: "\"{answer: 42}\"" 397 | }); 398 | }); 399 | }); 400 | 401 | it('(required) should add a validation error', function (done) { 402 | helper.load([componentStart, componentReturn, runComponent], flowValidations, {}, function () { 403 | var run = helper.getNode("04"); 404 | run.on("input", function (msg) { 405 | try { 406 | let expectedErrors = { 407 | validationErrors: { 408 | "req_string": "components.message.missingProperty", 409 | "req_number": "components.message.missingProperty", 410 | "req_boolean": "components.message.missingProperty", 411 | "req_json": "components.message.missingProperty", 412 | "req_any": "components.message.missingProperty", 413 | } 414 | }; 415 | run.error.should.be.calledWithExactly(expectedErrors); 416 | done(); 417 | } catch (e) { 418 | done(e); 419 | } 420 | }); 421 | run.receive({ 422 | payload: "test" 423 | }); 424 | }); 425 | }); 426 | 427 | }); -------------------------------------------------------------------------------- /components/test/unconnected_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require("node-red-node-test-helper"); 3 | var componentStart = require("../component-start.js"); 4 | var componentReturn = require("../component-return.js"); 5 | var runComponent = require("../run-component.js"); 6 | 7 | helper.init(require.resolve('node-red')); 8 | 9 | var flowWithUnconnectedInNode = [ 10 | { 11 | "id": "out01", 12 | "type": "component_out", 13 | "name": "ret 01", 14 | "mode": "separate", 15 | "wires": [] 16 | }, 17 | { 18 | "id": "run01", 19 | "type": "component", 20 | "z": "tab", 21 | "name": "run 01", 22 | "paramSources": {}, 23 | "statuz": "", 24 | "statuzType": "str", 25 | "outputs": 1, 26 | "outLabels": [ 27 | "default" 28 | ], 29 | "wires": [ 30 | [] 31 | ] 32 | }, 33 | ] 34 | 35 | describe('unconnected ', function () { 36 | 37 | before(function (done) { 38 | helper.startServer(done); 39 | }); 40 | 41 | after(function (done) { 42 | helper.stopServer(done); 43 | }); 44 | 45 | afterEach(function () { 46 | helper.unload(); 47 | }); 48 | 49 | it('out nodes should throw an error', function (done) { 50 | helper.load([componentStart, componentReturn, runComponent], flowWithUnconnectedInNode, {}, function () { 51 | const out01 = helper.getNode('out01') 52 | out01.error.should.be.called() 53 | 54 | out01.on('input', () => { 55 | out01.error.should.be.called() 56 | done(); 57 | }); 58 | out01.receive({ test: true }); 59 | }); 60 | }); 61 | 62 | }); -------------------------------------------------------------------------------- /components/uitest/editor_spec.js: -------------------------------------------------------------------------------- 1 | var puppeteer = require("puppeteer"); 2 | var http = require('http'); 3 | var express = require("express"); 4 | var RED = require("node-red"); 5 | 6 | let server; 7 | 8 | describe('nested components in the admin UI', function () { 9 | 10 | before(function (done) { 11 | // Create an Express app 12 | const app = express(); 13 | 14 | // Add a simple route for static content served from 'public' 15 | app.use("/", express.static("public")); 16 | 17 | // Create a server 18 | server = http.createServer(app); 19 | 20 | // Create the settings object - see default settings.js file for other options 21 | var settings = { 22 | httpAdminRoot: "/red", 23 | httpNodeRoot: "/api", 24 | userDir: __dirname, 25 | flowFile: "flows.json", 26 | functionGlobalContext: {} // enables global context 27 | }; 28 | 29 | // Initialise the runtime with a server and settings 30 | RED.init(server, settings); 31 | 32 | // Serve the editor UI from /red 33 | app.use(settings.httpAdminRoot, RED.httpAdmin); 34 | 35 | // Serve the http nodes UI from /api 36 | app.use(settings.httpNodeRoot, RED.httpNode); 37 | 38 | server.listen(8000); 39 | 40 | // Start the runtime 41 | RED.start(); 42 | 43 | done(); 44 | }); 45 | 46 | after(function (done) { 47 | RED.stop(); 48 | server.close(); 49 | done(); 50 | }); 51 | 52 | it('should basically work', function (done) { 53 | (async () => { 54 | const browser = await puppeteer.launch({ defaultViewport: { width: 1280, height: 1024 } }); 55 | const page = await browser.newPage(); 56 | await page.goto("http://localhost:8000/red/"); 57 | await page.waitForSelector("div .red-ui-tabs-search.red-ui-tab-button", { visible: true }); 58 | await page.screenshot({ path: 'node-red-admin-ui.png' }); 59 | 60 | // drag node 61 | let elm = await (await page.waitForSelector("[id='19eab3c2.0c8d8c']", { visible: true })); 62 | let bounding_box = await elm.boundingBox(); 63 | let x = bounding_box.x + bounding_box.width / 2; 64 | let y = bounding_box.y + bounding_box.height / 2; 65 | await page.mouse.move(x, y); 66 | await page.mouse.down(); 67 | await page.waitForTimeout(50); 68 | await page.mouse.move(x + 100, y, { steps: 10 }); 69 | await page.waitForTimeout(50); 70 | await page.mouse.up(); 71 | await page.waitForTimeout(50); 72 | 73 | await page.screenshot({ path: 'after-drag.png' }); 74 | 75 | 76 | await page.mouse.down(); 77 | await page.mouse.up(); 78 | await page.mouse.down(); 79 | await page.mouse.up(); 80 | 81 | await page.screenshot({ path: 'after-click.png' }); 82 | 83 | 84 | await browser.close(); 85 | done(); 86 | })() 87 | }).timeout(20000); 88 | }); -------------------------------------------------------------------------------- /components/uitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-project", 3 | "description": "A Node-RED Project", 4 | "version": "0.0.1", 5 | "private": true, 6 | "dependencies": { 7 | "node-red-contrib-components": "file:../../" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/uitest/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the default settings file provided by Node-RED. 3 | * 4 | * It can contain any valid JavaScript code that will get run when Node-RED 5 | * is started. 6 | * 7 | * Lines that start with // are commented out. 8 | * Each entry should be separated from the entries above and below by a comma ',' 9 | * 10 | * For more information about individual settings, refer to the documentation: 11 | * https://nodered.org/docs/user-guide/runtime/configuration 12 | * 13 | * The settings are split into the following sections: 14 | * - Flow File and User Directory Settings 15 | * - Security 16 | * - Server Settings 17 | * - Runtime Settings 18 | * - Editor Settings 19 | * - Node Settings 20 | * 21 | **/ 22 | 23 | module.exports = { 24 | 25 | /******************************************************************************* 26 | * Flow File and User Directory Settings 27 | * - flowFile 28 | * - credentialSecret 29 | * - flowFilePretty 30 | * - userDir 31 | * - nodesDir 32 | ******************************************************************************/ 33 | 34 | /** The file containing the flows. If not set, defaults to flows_.json **/ 35 | flowFile: 'flows.json', 36 | 37 | /** By default, credentials are encrypted in storage using a generated key. To 38 | * specify your own secret, set the following property. 39 | * If you want to disable encryption of credentials, set this property to false. 40 | * Note: once you set this property, do not change it - doing so will prevent 41 | * node-red from being able to decrypt your existing credentials and they will be 42 | * lost. 43 | */ 44 | //credentialSecret: "a-secret-key", 45 | 46 | /** By default, the flow JSON will be formatted over multiple lines making 47 | * it easier to compare changes when using version control. 48 | * To disable pretty-printing of the JSON set the following property to false. 49 | */ 50 | flowFilePretty: true, 51 | 52 | /** By default, all user data is stored in a directory called `.node-red` under 53 | * the user's home directory. To use a different location, the following 54 | * property can be used 55 | */ 56 | //userDir: '/home/nol/.node-red/', 57 | 58 | /** Node-RED scans the `nodes` directory in the userDir to find local node files. 59 | * The following property can be used to specify an additional directory to scan. 60 | */ 61 | //nodesDir: '/home/nol/.node-red/nodes', 62 | 63 | /******************************************************************************* 64 | * Security 65 | * - adminAuth 66 | * - https 67 | * - httpsRefreshInterval 68 | * - requireHttps 69 | * - httpNodeAuth 70 | * - httpStaticAuth 71 | ******************************************************************************/ 72 | 73 | /** To password protect the Node-RED editor and admin API, the following 74 | * property can be used. See http://nodered.org/docs/security.html for details. 75 | */ 76 | //adminAuth: { 77 | // type: "credentials", 78 | // users: [{ 79 | // username: "admin", 80 | // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.", 81 | // permissions: "*" 82 | // }] 83 | //}, 84 | 85 | /** The following property can be used to enable HTTPS 86 | * This property can be either an object, containing both a (private) key 87 | * and a (public) certificate, or a function that returns such an object. 88 | * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener 89 | * for details of its contents. 90 | */ 91 | 92 | /** Option 1: static object */ 93 | //https: { 94 | // key: require("fs").readFileSync('privkey.pem'), 95 | // cert: require("fs").readFileSync('cert.pem') 96 | //}, 97 | 98 | /** Option 2: function that returns the HTTP configuration object */ 99 | // https: function() { 100 | // // This function should return the options object, or a Promise 101 | // // that resolves to the options object 102 | // return { 103 | // key: require("fs").readFileSync('privkey.pem'), 104 | // cert: require("fs").readFileSync('cert.pem') 105 | // } 106 | // }, 107 | 108 | /** If the `https` setting is a function, the following setting can be used 109 | * to set how often, in hours, the function will be called. That can be used 110 | * to refresh any certificates. 111 | */ 112 | //httpsRefreshInterval : 12, 113 | 114 | /** The following property can be used to cause insecure HTTP connections to 115 | * be redirected to HTTPS. 116 | */ 117 | //requireHttps: true, 118 | 119 | /** To password protect the node-defined HTTP endpoints (httpNodeRoot), 120 | * including node-red-dashboard, or the static content (httpStatic), the 121 | * following properties can be used. 122 | * The `pass` field is a bcrypt hash of the password. 123 | * See http://nodered.org/docs/security.html#generating-the-password-hash 124 | */ 125 | //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, 126 | //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, 127 | 128 | /******************************************************************************* 129 | * Server Settings 130 | * - uiPort 131 | * - uiHost 132 | * - apiMaxLength 133 | * - httpServerOptions 134 | * - httpAdminRoot 135 | * - httpAdminMiddleware 136 | * - httpNodeRoot 137 | * - httpNodeCors 138 | * - httpNodeMiddleware 139 | * - httpStatic 140 | ******************************************************************************/ 141 | 142 | /** the tcp port that the Node-RED web server is listening on */ 143 | uiPort: process.env.PORT || 1880, 144 | 145 | /** By default, the Node-RED UI accepts connections on all IPv4 interfaces. 146 | * To listen on all IPv6 addresses, set uiHost to "::", 147 | * The following property can be used to listen on a specific interface. For 148 | * example, the following would only allow connections from the local machine. 149 | */ 150 | //uiHost: "127.0.0.1", 151 | 152 | /** The maximum size of HTTP request that will be accepted by the runtime api. 153 | * Default: 5mb 154 | */ 155 | //apiMaxLength: '5mb', 156 | 157 | /** The following property can be used to pass custom options to the Express.js 158 | * server used by Node-RED. For a full list of available options, refer 159 | * to http://expressjs.com/en/api.html#app.settings.table 160 | */ 161 | //httpServerOptions: { }, 162 | 163 | /** By default, the Node-RED UI is available at http://localhost:1880/ 164 | * The following property can be used to specify a different root path. 165 | * If set to false, this is disabled. 166 | */ 167 | //httpAdminRoot: '/admin', 168 | 169 | /** The following property can be used to add a custom middleware function 170 | * in front of all admin http routes. For example, to set custom http 171 | * headers. It can be a single function or an array of middleware functions. 172 | */ 173 | // httpAdminMiddleware: function(req,res,next) { 174 | // // Set the X-Frame-Options header to limit where the editor 175 | // // can be embedded 176 | // //res.set('X-Frame-Options', 'sameorigin'); 177 | // next(); 178 | // }, 179 | 180 | 181 | /** Some nodes, such as HTTP In, can be used to listen for incoming http requests. 182 | * By default, these are served relative to '/'. The following property 183 | * can be used to specifiy a different root path. If set to false, this is 184 | * disabled. 185 | */ 186 | //httpNodeRoot: '/red-nodes', 187 | 188 | /** The following property can be used to configure cross-origin resource sharing 189 | * in the HTTP nodes. 190 | * See https://github.com/troygoode/node-cors#configuration-options for 191 | * details on its contents. The following is a basic permissive set of options: 192 | */ 193 | //httpNodeCors: { 194 | // origin: "*", 195 | // methods: "GET,PUT,POST,DELETE" 196 | //}, 197 | 198 | /** If you need to set an http proxy please set an environment variable 199 | * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. 200 | * For example - http_proxy=http://myproxy.com:8080 201 | * (Setting it here will have no effect) 202 | * You may also specify no_proxy (or NO_PROXY) to supply a comma separated 203 | * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk 204 | */ 205 | 206 | /** The following property can be used to add a custom middleware function 207 | * in front of all http in nodes. This allows custom authentication to be 208 | * applied to all http in nodes, or any other sort of common request processing. 209 | * It can be a single function or an array of middleware functions. 210 | */ 211 | //httpNodeMiddleware: function(req,res,next) { 212 | // // Handle/reject the request, or pass it on to the http in node by calling next(); 213 | // // Optionally skip our rawBodyParser by setting this to true; 214 | // //req.skipRawBodyParser = true; 215 | // next(); 216 | //}, 217 | 218 | /** When httpAdminRoot is used to move the UI to a different root path, the 219 | * following property can be used to identify a directory of static content 220 | * that should be served at http://localhost:1880/. 221 | */ 222 | //httpStatic: '/home/nol/node-red-static/', 223 | 224 | /******************************************************************************* 225 | * Runtime Settings 226 | * - lang 227 | * - logging 228 | * - contextStorage 229 | * - exportGlobalContextKeys 230 | * - externalModules 231 | ******************************************************************************/ 232 | 233 | /** Uncomment the following to run node-red in your preferred language. 234 | * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko 235 | * Some languages are more complete than others. 236 | */ 237 | // lang: "de", 238 | 239 | /** Configure the logging output */ 240 | logging: { 241 | /** Only console logging is currently supported */ 242 | console: { 243 | /** Level of logging to be recorded. Options are: 244 | * fatal - only those errors which make the application unusable should be recorded 245 | * error - record errors which are deemed fatal for a particular request + fatal errors 246 | * warn - record problems which are non fatal + errors + fatal errors 247 | * info - record information about the general running of the application + warn + error + fatal errors 248 | * debug - record information which is more verbose than info + info + warn + error + fatal errors 249 | * trace - record very detailed logging + debug + info + warn + error + fatal errors 250 | * off - turn off all logging (doesn't affect metrics or audit) 251 | */ 252 | level: "info", 253 | /** Whether or not to include metric events in the log output */ 254 | metrics: false, 255 | /** Whether or not to include audit events in the log output */ 256 | audit: false 257 | } 258 | }, 259 | 260 | /** Context Storage 261 | * The following property can be used to enable context storage. The configuration 262 | * provided here will enable file-based context that flushes to disk every 30 seconds. 263 | * Refer to the documentation for further options: https://nodered.org/docs/api/context/ 264 | */ 265 | //contextStorage: { 266 | // default: { 267 | // module:"localfilesystem" 268 | // }, 269 | //}, 270 | 271 | /** `global.keys()` returns a list of all properties set in global context. 272 | * This allows them to be displayed in the Context Sidebar within the editor. 273 | * In some circumstances it is not desirable to expose them to the editor. The 274 | * following property can be used to hide any property set in `functionGlobalContext` 275 | * from being list by `global.keys()`. 276 | * By default, the property is set to false to avoid accidental exposure of 277 | * their values. Setting this to true will cause the keys to be listed. 278 | */ 279 | exportGlobalContextKeys: false, 280 | 281 | /** Configure how the runtime will handle external npm modules. 282 | * This covers: 283 | * - whether the editor will allow new node modules to be installed 284 | * - whether nodes, such as the Function node are allowed to have their 285 | * own dynamically configured dependencies. 286 | * The allow/denyList options can be used to limit what modules the runtime 287 | * will install/load. It can use '*' as a wildcard that matches anything. 288 | */ 289 | externalModules: { 290 | // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ 291 | // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ 292 | // palette: { /** Configuration for the Palette Manager */ 293 | // allowInstall: true, /** Enable the Palette Manager in the editor */ 294 | // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ 295 | // allowList: [], 296 | // denyList: [] 297 | // }, 298 | // modules: { /** Configuration for node-specified modules */ 299 | // allowInstall: true, 300 | // allowList: [], 301 | // denyList: [] 302 | // } 303 | }, 304 | 305 | 306 | /******************************************************************************* 307 | * Editor Settings 308 | * - disableEditor 309 | * - editorTheme 310 | ******************************************************************************/ 311 | 312 | /** The following property can be used to disable the editor. The admin API 313 | * is not affected by this option. To disable both the editor and the admin 314 | * API, use either the httpRoot or httpAdminRoot properties 315 | */ 316 | //disableEditor: false, 317 | 318 | /** Customising the editor 319 | * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes 320 | * for all available options. 321 | */ 322 | editorTheme: { 323 | /** The following property can be used to set a custom theme for the editor. 324 | * See https://github.com/node-red-contrib-themes/theme-collection for 325 | * a collection of themes to chose from. 326 | */ 327 | //theme: "", 328 | palette: { 329 | /** The following property can be used to order the categories in the editor 330 | * palette. If a node's category is not in the list, the category will get 331 | * added to the end of the palette. 332 | * If not set, the following default order is used: 333 | */ 334 | //categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], 335 | }, 336 | projects: { 337 | /** To enable the Projects feature, set this value to true */ 338 | enabled: false, 339 | workflow: { 340 | /** Set the default projects workflow mode. 341 | * - manual - you must manually commit changes 342 | * - auto - changes are automatically committed 343 | * This can be overridden per-user from the 'Git config' 344 | * section of 'User Settings' within the editor 345 | */ 346 | mode: "manual" 347 | } 348 | }, 349 | codeEditor: { 350 | /** Select the text editor component used by the editor. 351 | * Defaults to "ace", but can be set to "ace" or "monaco" 352 | */ 353 | lib: "ace", 354 | options: { 355 | /** The follow options only apply if the editor is set to "monaco" 356 | * 357 | * theme - must match the file name of a theme in 358 | * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme 359 | * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme" 360 | */ 361 | theme: "vs", 362 | /** other overrides can be set e.g. fontSize, fontFamily, fontLigatures etc. 363 | * for the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html 364 | */ 365 | //fontSize: 14, 366 | //fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", 367 | //fontLigatures: true, 368 | } 369 | } 370 | }, 371 | 372 | /******************************************************************************* 373 | * Node Settings 374 | * - fileWorkingDirectory 375 | * - functionGlobalContext 376 | * - functionExternalModules 377 | * - nodeMessageBufferMaxLength 378 | * - ui (for use with Node-RED Dashboard) 379 | * - debugUseColors 380 | * - debugMaxLength 381 | * - execMaxBufferSize 382 | * - httpRequestTimeout 383 | * - mqttReconnectTime 384 | * - serialReconnectTime 385 | * - socketReconnectTime 386 | * - socketTimeout 387 | * - tcpMsgQueueSize 388 | * - inboundWebSocketTimeout 389 | * - tlsConfigDisableLocalFiles 390 | * - webSocketNodeVerifyClient 391 | ******************************************************************************/ 392 | 393 | /** The working directory to handle relative file paths from within the File nodes 394 | * defaults to the working directory of the Node-RED process. 395 | */ 396 | //fileWorkingDirectory: "", 397 | 398 | /** Allow the Function node to load additional npm modules directly */ 399 | functionExternalModules: true, 400 | 401 | /** The following property can be used to set predefined values in Global Context. 402 | * This allows extra node modules to be made available with in Function node. 403 | * For example, the following: 404 | * functionGlobalContext: { os:require('os') } 405 | * will allow the `os` module to be accessed in a Function node using: 406 | * global.get("os") 407 | */ 408 | functionGlobalContext: { 409 | // os:require('os'), 410 | }, 411 | 412 | /** The maximum number of messages nodes will buffer internally as part of their 413 | * operation. This applies across a range of nodes that operate on message sequences. 414 | * defaults to no limit. A value of 0 also means no limit is applied. 415 | */ 416 | //nodeMessageBufferMaxLength: 0, 417 | 418 | /** If you installed the optional node-red-dashboard you can set it's path 419 | * relative to httpNodeRoot 420 | * Other optional properties include 421 | * readOnly:{boolean}, 422 | * middleware:{function or array}, (req,res,next) - http middleware 423 | * ioMiddleware:{function or array}, (socket,next) - socket.io middleware 424 | */ 425 | //ui: { path: "ui" }, 426 | 427 | /** Colourise the console output of the debug node */ 428 | //debugUseColors: true, 429 | 430 | /** The maximum length, in characters, of any message sent to the debug sidebar tab */ 431 | debugMaxLength: 1000, 432 | 433 | /** Maximum buffer size for the exec node. Defaults to 10Mb */ 434 | //execMaxBufferSize: 10000000, 435 | 436 | /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ 437 | //httpRequestTimeout: 120000, 438 | 439 | /** Retry time in milliseconds for MQTT connections */ 440 | mqttReconnectTime: 15000, 441 | 442 | /** Retry time in milliseconds for Serial port connections */ 443 | serialReconnectTime: 15000, 444 | 445 | /** Retry time in milliseconds for TCP socket connections */ 446 | //socketReconnectTime: 10000, 447 | 448 | /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ 449 | //socketTimeout: 120000, 450 | 451 | /** Maximum number of messages to wait in queue while attempting to connect to TCP socket 452 | * defaults to 1000 453 | */ 454 | //tcpMsgQueueSize: 2000, 455 | 456 | /** Timeout in milliseconds for inbound WebSocket connections that do not 457 | * match any configured node. Defaults to 5000 458 | */ 459 | //inboundWebSocketTimeout: 5000, 460 | 461 | /** To disable the option for using local files for storing keys and 462 | * certificates in the TLS configuration node, set this to true. 463 | */ 464 | //tlsConfigDisableLocalFiles: true, 465 | 466 | /** The following property can be used to verify websocket connection attempts. 467 | * This allows, for example, the HTTP request headers to be checked to ensure 468 | * they include valid authentication information. 469 | */ 470 | //webSocketNodeVerifyClient: function(info) { 471 | // /** 'info' has three properties: 472 | // * - origin : the value in the Origin header 473 | // * - req : the HTTP request 474 | // * - secure : true if req.connection.authorized or req.connection.encrypted is set 475 | // * 476 | // * The function should return true if the connection should be accepted, false otherwise. 477 | // * 478 | // * Alternatively, if this function is defined to accept a second argument, callback, 479 | // * it can be used to verify the client asynchronously. 480 | // * The callback takes three arguments: 481 | // * - result : boolean, whether to accept the connection or not 482 | // * - code : if result is false, the HTTP error status to return 483 | // * - reason: if result is false, the HTTP reason string to return 484 | // */ 485 | //}, 486 | } 487 | -------------------------------------------------------------------------------- /examples/basic.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "fbb8e0c6.fd63c", 4 | "type": "component_in", 5 | "z": "5384bb85.537fd4", 6 | "name": "Component", 7 | "api": [], 8 | "x": 170, 9 | "y": 640, 10 | "wires": [ 11 | [ 12 | "19eab3c2.0c8d8c" 13 | ] 14 | ] 15 | }, 16 | { 17 | "id": "19eab3c2.0c8d8c", 18 | "type": "component_out", 19 | "z": "5384bb85.537fd4", 20 | "name": "return", 21 | "mode": "default", 22 | "x": 450, 23 | "y": 640, 24 | "wires": [] 25 | }, 26 | { 27 | "id": "65025951.68c2a8", 28 | "type": "component", 29 | "z": "5384bb85.537fd4", 30 | "name": "run it", 31 | "targetComponent": { 32 | "id": "fbb8e0c6.fd63c", 33 | "name": "Component", 34 | "api": [] 35 | }, 36 | "paramSources": {}, 37 | "statuz": "", 38 | "statuzType": "str", 39 | "outputs": 1, 40 | "outLabels": [ 41 | "default" 42 | ], 43 | "x": 290, 44 | "y": 600, 45 | "wires": [ 46 | [ 47 | "364e4d91.1c1952" 48 | ] 49 | ] 50 | }, 51 | { 52 | "id": "364e4d91.1c1952", 53 | "type": "debug", 54 | "z": "5384bb85.537fd4", 55 | "name": "", 56 | "active": true, 57 | "tosidebar": true, 58 | "console": false, 59 | "tostatus": false, 60 | "complete": "true", 61 | "targetType": "full", 62 | "statusVal": "", 63 | "statusType": "auto", 64 | "x": 450, 65 | "y": 600, 66 | "wires": [] 67 | }, 68 | { 69 | "id": "4398a730.4ea3e8", 70 | "type": "inject", 71 | "z": "5384bb85.537fd4", 72 | "name": "Run", 73 | "props": [ 74 | { 75 | "p": "payload" 76 | } 77 | ], 78 | "repeat": "", 79 | "crontab": "", 80 | "once": false, 81 | "onceDelay": 0.1, 82 | "topic": "", 83 | "payload": "Works!", 84 | "payloadType": "str", 85 | "x": 150, 86 | "y": 600, 87 | "wires": [ 88 | [ 89 | "65025951.68c2a8" 90 | ] 91 | ] 92 | } 93 | ] -------------------------------------------------------------------------------- /examples/broadcast.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1598253f50d21089", 4 | "type": "component", 5 | "z": "6542e73107b394b8", 6 | "name": "", 7 | "targetComponent": { 8 | "id": "d45aeacd26a2b94b", 9 | "name": "broadcast", 10 | "api": [] 11 | }, 12 | "paramSources": {}, 13 | "statuz": "", 14 | "statuzType": "str", 15 | "outputs": 1, 16 | "outLabels": [ 17 | "default" 18 | ], 19 | "x": 390, 20 | "y": 280, 21 | "wires": [ 22 | [ 23 | "190830f284bc3b77" 24 | ] 25 | ] 26 | }, 27 | { 28 | "id": "d1a15eb79388c309", 29 | "type": "component_out", 30 | "z": "6542e73107b394b8", 31 | "name": "", 32 | "mode": "default", 33 | "node_is_not_connected": false, 34 | "component_definitions_are_NOT_allowed_inside_subflows": false, 35 | "x": 390, 36 | "y": 360, 37 | "wires": [] 38 | }, 39 | { 40 | "id": "d45aeacd26a2b94b", 41 | "type": "component_in", 42 | "z": "6542e73107b394b8", 43 | "name": "broadcast", 44 | "api": [], 45 | "node_is_not_connected": false, 46 | "x": 180, 47 | "y": 360, 48 | "wires": [ 49 | [ 50 | "d1a15eb79388c309" 51 | ] 52 | ] 53 | }, 54 | { 55 | "id": "190830f284bc3b77", 56 | "type": "debug", 57 | "z": "6542e73107b394b8", 58 | "name": "", 59 | "active": true, 60 | "tosidebar": true, 61 | "console": false, 62 | "tostatus": false, 63 | "complete": "false", 64 | "statusVal": "", 65 | "statusType": "auto", 66 | "x": 570, 67 | "y": 280, 68 | "wires": [] 69 | }, 70 | { 71 | "id": "4c1676fb7cfeb5f3", 72 | "type": "inject", 73 | "z": "6542e73107b394b8", 74 | "name": "", 75 | "props": [ 76 | { 77 | "p": "payload" 78 | } 79 | ], 80 | "repeat": "", 81 | "crontab": "", 82 | "once": false, 83 | "onceDelay": 0.1, 84 | "topic": "", 85 | "payload": "Test", 86 | "payloadType": "str", 87 | "x": 190, 88 | "y": 420, 89 | "wires": [ 90 | [ 91 | "d1a15eb79388c309" 92 | ] 93 | ] 94 | }, 95 | { 96 | "id": "d4b4021db0d2a37e", 97 | "type": "component", 98 | "z": "6542e73107b394b8", 99 | "name": "", 100 | "targetComponent": { 101 | "id": "d45aeacd26a2b94b", 102 | "name": "broadcast", 103 | "api": [] 104 | }, 105 | "paramSources": {}, 106 | "statuz": "", 107 | "statuzType": "str", 108 | "outputs": 1, 109 | "outLabels": [ 110 | "default" 111 | ], 112 | "x": 390, 113 | "y": 320, 114 | "wires": [ 115 | [ 116 | "e987d8a0fb7fc8bd" 117 | ] 118 | ] 119 | }, 120 | { 121 | "id": "e987d8a0fb7fc8bd", 122 | "type": "debug", 123 | "z": "6542e73107b394b8", 124 | "name": "", 125 | "active": true, 126 | "tosidebar": true, 127 | "console": false, 128 | "tostatus": false, 129 | "complete": "false", 130 | "statusVal": "", 131 | "statusType": "auto", 132 | "x": 570, 133 | "y": 320, 134 | "wires": [] 135 | } 136 | ] -------------------------------------------------------------------------------- /examples/embedded.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "dcdfa483.b6b2d8", 4 | "type": "component_in", 5 | "z": "5384bb85.537fd4", 6 | "name": "Component 1", 7 | "api": [ 8 | { 9 | "name": "name1", 10 | "type": "string", 11 | "required": true 12 | }, 13 | { 14 | "name": "Das ist ein längerer", 15 | "type": "json", 16 | "required": true 17 | }, 18 | { 19 | "name": "number", 20 | "type": "number", 21 | "required": true 22 | }, 23 | { 24 | "name": "bool", 25 | "type": "boolean", 26 | "required": true 27 | }, 28 | { 29 | "name": "sdfsdfdfsd", 30 | "type": "any", 31 | "required": true 32 | } 33 | ], 34 | "x": 170, 35 | "y": 320, 36 | "wires": [ 37 | [ 38 | "f18f31a4.2888b" 39 | ] 40 | ] 41 | }, 42 | { 43 | "id": "56b92b19.e59524", 44 | "type": "component_out", 45 | "z": "5384bb85.537fd4", 46 | "name": "ret 01a", 47 | "mode": "default", 48 | "x": 710, 49 | "y": 320, 50 | "wires": [] 51 | }, 52 | { 53 | "id": "dc96eeae.03b13", 54 | "type": "component", 55 | "z": "5384bb85.537fd4", 56 | "name": "run 02", 57 | "targetComponent": { 58 | "id": "30f5fb76.8401c4", 59 | "name": "Component 2", 60 | "api": [] 61 | }, 62 | "paramSources": {}, 63 | "statuz": "", 64 | "statuzType": "str", 65 | "outputs": 1, 66 | "outLabels": [ 67 | "ret 02" 68 | ], 69 | "x": 550, 70 | "y": 320, 71 | "wires": [ 72 | [ 73 | "56b92b19.e59524" 74 | ] 75 | ] 76 | }, 77 | { 78 | "id": "30f5fb76.8401c4", 79 | "type": "component_in", 80 | "z": "5384bb85.537fd4", 81 | "name": "Component 2", 82 | "api": [], 83 | "x": 170, 84 | "y": 420, 85 | "wires": [ 86 | [ 87 | "f894bb4.9489448" 88 | ] 89 | ] 90 | }, 91 | { 92 | "id": "ead6ba0d.80dd18", 93 | "type": "component_out", 94 | "z": "5384bb85.537fd4", 95 | "name": "ret 02", 96 | "mode": "separate", 97 | "x": 670, 98 | "y": 420, 99 | "wires": [] 100 | }, 101 | { 102 | "id": "f18f31a4.2888b", 103 | "type": "change", 104 | "z": "5384bb85.537fd4", 105 | "name": "", 106 | "rules": [ 107 | { 108 | "t": "set", 109 | "p": "outer", 110 | "pt": "msg", 111 | "to": "{\"test\": 42}", 112 | "tot": "json" 113 | } 114 | ], 115 | "action": "", 116 | "property": "", 117 | "from": "", 118 | "to": "", 119 | "reg": false, 120 | "x": 360, 121 | "y": 320, 122 | "wires": [ 123 | [ 124 | "dc96eeae.03b13", 125 | "1c65437f.5d316d" 126 | ] 127 | ] 128 | }, 129 | { 130 | "id": "8b154ce9.4db91", 131 | "type": "inject", 132 | "z": "5384bb85.537fd4", 133 | "name": "Run", 134 | "props": [ 135 | { 136 | "p": "payload" 137 | } 138 | ], 139 | "repeat": "", 140 | "crontab": "", 141 | "once": false, 142 | "onceDelay": 0.1, 143 | "topic": "", 144 | "payload": "{\"inner\":{\"more\":\"Hey\",\"even more\":999}}", 145 | "payloadType": "json", 146 | "x": 170, 147 | "y": 240, 148 | "wires": [ 149 | [ 150 | "1cf63259.f760be" 151 | ] 152 | ] 153 | }, 154 | { 155 | "id": "5900835f.17f83c", 156 | "type": "debug", 157 | "z": "5384bb85.537fd4", 158 | "name": "default (inner)", 159 | "active": true, 160 | "tosidebar": true, 161 | "console": false, 162 | "tostatus": false, 163 | "complete": "true", 164 | "targetType": "full", 165 | "statusVal": "", 166 | "statusType": "auto", 167 | "x": 520, 168 | "y": 220, 169 | "wires": [] 170 | }, 171 | { 172 | "id": "f894bb4.9489448", 173 | "type": "change", 174 | "z": "5384bb85.537fd4", 175 | "name": "", 176 | "rules": [ 177 | { 178 | "t": "set", 179 | "p": "inner", 180 | "pt": "msg", 181 | "to": "23", 182 | "tot": "json" 183 | } 184 | ], 185 | "action": "", 186 | "property": "", 187 | "from": "", 188 | "to": "", 189 | "reg": false, 190 | "x": 360, 191 | "y": 420, 192 | "wires": [ 193 | [ 194 | "f250b246.6d27c" 195 | ] 196 | ] 197 | }, 198 | { 199 | "id": "1c65437f.5d316d", 200 | "type": "component_out", 201 | "z": "5384bb85.537fd4", 202 | "name": "ret 01b", 203 | "mode": "separate", 204 | "x": 560, 205 | "y": 360, 206 | "wires": [] 207 | }, 208 | { 209 | "id": "1cf63259.f760be", 210 | "type": "component", 211 | "z": "5384bb85.537fd4", 212 | "name": "run 01", 213 | "targetComponent": { 214 | "id": "dcdfa483.b6b2d8", 215 | "name": "Component 1", 216 | "api": [ 217 | { 218 | "name": "name1", 219 | "type": "string", 220 | "required": true 221 | }, 222 | { 223 | "name": "Das ist ein längerer", 224 | "type": "json", 225 | "required": true 226 | }, 227 | { 228 | "name": "number", 229 | "type": "number", 230 | "required": true 231 | }, 232 | { 233 | "name": "bool", 234 | "type": "boolean", 235 | "required": true 236 | }, 237 | { 238 | "name": "sdfsdfdfsd", 239 | "type": "any", 240 | "required": true 241 | } 242 | ] 243 | }, 244 | "paramSources": { 245 | "name1": { 246 | "name": "name1", 247 | "type": "string", 248 | "required": true, 249 | "source": "\"Test\"", 250 | "sourceType": "jsonata" 251 | }, 252 | "Das ist ein längerer": { 253 | "name": "Das ist ein längerer", 254 | "type": "json", 255 | "required": true, 256 | "source": "[\"a\", \"b\"]", 257 | "sourceType": "json" 258 | }, 259 | "number": { 260 | "name": "number", 261 | "type": "number", 262 | "required": true, 263 | "source": "4", 264 | "sourceType": "json" 265 | }, 266 | "bool": { 267 | "name": "bool", 268 | "type": "boolean", 269 | "required": true, 270 | "source": "true", 271 | "sourceType": "bool" 272 | }, 273 | "sdfsdfdfsd": { 274 | "name": "sdfsdfdfsd", 275 | "type": "any", 276 | "required": true, 277 | "source": "{}", 278 | "sourceType": "json" 279 | } 280 | }, 281 | "statuz": "name1", 282 | "statuzType": "msg", 283 | "outputs": 2, 284 | "outLabels": [ 285 | "default", 286 | "ret 01b" 287 | ], 288 | "x": 330, 289 | "y": 240, 290 | "wires": [ 291 | [ 292 | "5900835f.17f83c" 293 | ], 294 | [ 295 | "ee4645bc.e3f438" 296 | ] 297 | ] 298 | }, 299 | { 300 | "id": "e2a3926c.6e823", 301 | "type": "link in", 302 | "z": "5384bb85.537fd4", 303 | "name": "link in 01", 304 | "links": [ 305 | "f250b246.6d27c" 306 | ], 307 | "x": 575, 308 | "y": 420, 309 | "wires": [ 310 | [ 311 | "ead6ba0d.80dd18" 312 | ] 313 | ] 314 | }, 315 | { 316 | "id": "f250b246.6d27c", 317 | "type": "link out", 318 | "z": "5384bb85.537fd4", 319 | "name": "link out 01", 320 | "links": [ 321 | "e2a3926c.6e823" 322 | ], 323 | "x": 475, 324 | "y": 420, 325 | "wires": [] 326 | }, 327 | { 328 | "id": "ee4645bc.e3f438", 329 | "type": "debug", 330 | "z": "5384bb85.537fd4", 331 | "name": "outer only", 332 | "active": true, 333 | "tosidebar": true, 334 | "console": false, 335 | "tostatus": false, 336 | "complete": "true", 337 | "targetType": "full", 338 | "statusVal": "", 339 | "statusType": "auto", 340 | "x": 500, 341 | "y": 260, 342 | "wires": [] 343 | } 344 | ] -------------------------------------------------------------------------------- /examples/in-only.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "bc4a543c71e49c3d", 4 | "type": "component_in", 5 | "z": "6542e73107b394b8", 6 | "name": "black hole", 7 | "api": [ 8 | { 9 | "name": "prop", 10 | "type": "string", 11 | "required": true, 12 | "global": false 13 | } 14 | ], 15 | "node_is_not_connected": false, 16 | "x": 190, 17 | "y": 140, 18 | "wires": [ 19 | [ 20 | "48d59f4c3f8bf29c" 21 | ] 22 | ] 23 | }, 24 | { 25 | "id": "48d59f4c3f8bf29c", 26 | "type": "change", 27 | "z": "6542e73107b394b8", 28 | "name": "", 29 | "rules": [ 30 | { 31 | "t": "set", 32 | "p": "blackhole", 33 | "pt": "global", 34 | "to": "prop", 35 | "tot": "msg" 36 | } 37 | ], 38 | "action": "", 39 | "property": "", 40 | "from": "", 41 | "to": "", 42 | "reg": false, 43 | "x": 400, 44 | "y": 140, 45 | "wires": [ 46 | [ 47 | "f54ff2eeef33e0a4" 48 | ] 49 | ] 50 | }, 51 | { 52 | "id": "893952bc6439e999", 53 | "type": "inject", 54 | "z": "6542e73107b394b8", 55 | "name": "", 56 | "props": [ 57 | { 58 | "p": "payload" 59 | } 60 | ], 61 | "repeat": "", 62 | "crontab": "", 63 | "once": false, 64 | "onceDelay": 0.1, 65 | "topic": "", 66 | "payload": "Test", 67 | "payloadType": "str", 68 | "x": 190, 69 | "y": 100, 70 | "wires": [ 71 | [ 72 | "3d4e4f07f5e86b93" 73 | ] 74 | ] 75 | }, 76 | { 77 | "id": "3d4e4f07f5e86b93", 78 | "type": "component", 79 | "z": "6542e73107b394b8", 80 | "name": "", 81 | "targetComponent": { 82 | "id": "bc4a543c71e49c3d", 83 | "name": "black hole", 84 | "api": [ 85 | { 86 | "name": "prop", 87 | "type": "string", 88 | "required": true, 89 | "global": false 90 | } 91 | ] 92 | }, 93 | "paramSources": { 94 | "prop": { 95 | "name": "prop", 96 | "type": "string", 97 | "required": true, 98 | "global": false, 99 | "source": "payload", 100 | "sourceType": "msg" 101 | } 102 | }, 103 | "statuz": "", 104 | "statuzType": "str", 105 | "outputs": 0, 106 | "outLabels": [], 107 | "x": 380, 108 | "y": 100, 109 | "wires": [] 110 | }, 111 | { 112 | "id": "f54ff2eeef33e0a4", 113 | "type": "debug", 114 | "z": "6542e73107b394b8", 115 | "name": "", 116 | "active": true, 117 | "tosidebar": true, 118 | "console": false, 119 | "tostatus": false, 120 | "complete": "true", 121 | "targetType": "full", 122 | "statusVal": "", 123 | "statusType": "auto", 124 | "x": 590, 125 | "y": 140, 126 | "wires": [] 127 | } 128 | ] -------------------------------------------------------------------------------- /examples/recursion.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "a6a8a672c69ba580", 4 | "type": "component_in", 5 | "z": "aa8d4fd1ad84c1f0", 6 | "name": "handle", 7 | "api": [ 8 | { 9 | "name": "prop", 10 | "type": "json", 11 | "required": true, 12 | "contextOption": true 13 | } 14 | ], 15 | "usecontext": true, 16 | "x": 110, 17 | "y": 280, 18 | "wires": [ 19 | [ 20 | "9005eef32143a5a4" 21 | ] 22 | ] 23 | }, 24 | { 25 | "id": "a59a865f4082b600", 26 | "type": "component", 27 | "z": "aa8d4fd1ad84c1f0", 28 | "name": "", 29 | "targetComponentId": "a6a8a672c69ba580", 30 | "paramSources": { 31 | "prop": { 32 | "name": "prop", 33 | "source": "payload.1", 34 | "sourceType": "msg" 35 | } 36 | }, 37 | "statuz": "", 38 | "statuzType": "str", 39 | "outputs": 1, 40 | "outLabels": [ 41 | "default" 42 | ], 43 | "x": 260, 44 | "y": 220, 45 | "wires": [ 46 | [ 47 | "acd9bbf168cc74e5" 48 | ] 49 | ] 50 | }, 51 | { 52 | "id": "2e073cef2ff94909", 53 | "type": "inject", 54 | "z": "aa8d4fd1ad84c1f0", 55 | "name": "", 56 | "props": [ 57 | { 58 | "p": "payload" 59 | } 60 | ], 61 | "repeat": "", 62 | "crontab": "", 63 | "once": false, 64 | "onceDelay": 0.1, 65 | "topic": "", 66 | "payload": "{\"1\":{\"name\":\"1\",\"children\":{\"1.1\":{\"name\":\"1.1\",\"children\":{\"1.1.1\":{\"name\":\"1.1.1\",\"children\":{}},\"1.1.2\":{\"name\":\"1.1.2\",\"children\":{}}}},\"1.2\":{\"name\":\"1.2\",\"children\":{\"1.2.1\":{\"name\":\"1.2.1\",\"children\":{}}}}}}}", 67 | "payloadType": "json", 68 | "x": 110, 69 | "y": 220, 70 | "wires": [ 71 | [ 72 | "a59a865f4082b600" 73 | ] 74 | ] 75 | }, 76 | { 77 | "id": "9005eef32143a5a4", 78 | "type": "change", 79 | "z": "aa8d4fd1ad84c1f0", 80 | "name": "", 81 | "rules": [ 82 | { 83 | "t": "set", 84 | "p": "component.prop.touch", 85 | "pt": "msg", 86 | "to": "true", 87 | "tot": "bool" 88 | } 89 | ], 90 | "action": "", 91 | "property": "", 92 | "from": "", 93 | "to": "", 94 | "reg": false, 95 | "x": 340, 96 | "y": 280, 97 | "wires": [ 98 | [ 99 | "cc5bb0dfa0bd902a" 100 | ] 101 | ] 102 | }, 103 | { 104 | "id": "cc5bb0dfa0bd902a", 105 | "type": "function", 106 | "z": "aa8d4fd1ad84c1f0", 107 | "name": "", 108 | "func": "if (msg.component.handledChildren) {\n // here we get the result of the nested handle call.\n if (msg.component.handledChildren.length == Object.keys(msg.component.prop.children).length) {\n delete msg.component.handledChildren;\n return msg;\n } else {\n // next child\n for (let c in msg.component.prop.children) {\n let child = msg.component.prop.children[c];\n if (msg.component.handledChildren.includes(child)) {\n continue;\n }\n msg.component.child = child;\n node.send([null, msg]);\n break;\n }\n }\n} else {\n // here the initial call from the start node\n let foundChild = false;\n for (let c in msg.component.prop.children) {\n let child = msg.component.prop.children[c];\n msg.component.child = child;\n msg.component.handledChildren = [];\n node.send([null, msg]);\n foundChild = true;\n break;\n }\n if (!foundChild) {\n return msg;\n }\n}\n", 109 | "outputs": 2, 110 | "noerr": 0, 111 | "initialize": "", 112 | "finalize": "", 113 | "libs": [], 114 | "x": 580, 115 | "y": 280, 116 | "wires": [ 117 | [ 118 | "711b5a0e375baea0" 119 | ], 120 | [ 121 | "620940be4a49e8d6" 122 | ] 123 | ] 124 | }, 125 | { 126 | "id": "620940be4a49e8d6", 127 | "type": "component", 128 | "z": "aa8d4fd1ad84c1f0", 129 | "name": "", 130 | "targetComponentId": "a6a8a672c69ba580", 131 | "paramSources": { 132 | "prop": { 133 | "name": "prop", 134 | "source": "component.child", 135 | "sourceType": "msg" 136 | } 137 | }, 138 | "statuz": "", 139 | "statuzType": "str", 140 | "outputs": 1, 141 | "outLabels": [ 142 | "default" 143 | ], 144 | "x": 760, 145 | "y": 340, 146 | "wires": [ 147 | [ 148 | "7c9ee2427d8a6734" 149 | ] 150 | ] 151 | }, 152 | { 153 | "id": "acd9bbf168cc74e5", 154 | "type": "debug", 155 | "z": "aa8d4fd1ad84c1f0", 156 | "name": "", 157 | "active": true, 158 | "tosidebar": true, 159 | "console": false, 160 | "tostatus": false, 161 | "complete": "false", 162 | "statusVal": "", 163 | "statusType": "auto", 164 | "x": 430, 165 | "y": 220, 166 | "wires": [] 167 | }, 168 | { 169 | "id": "7c9ee2427d8a6734", 170 | "type": "function", 171 | "z": "aa8d4fd1ad84c1f0", 172 | "name": "mark as handled", 173 | "func": "if (msg.component.handledChildren) {\n msg.component.handledChildren.push(msg.component.child);\n}\nreturn msg;", 174 | "outputs": 1, 175 | "noerr": 0, 176 | "initialize": "", 177 | "finalize": "", 178 | "libs": [], 179 | "x": 930, 180 | "y": 340, 181 | "wires": [ 182 | [ 183 | "cc5bb0dfa0bd902a" 184 | ] 185 | ] 186 | }, 187 | { 188 | "id": "711b5a0e375baea0", 189 | "type": "component_out", 190 | "z": "aa8d4fd1ad84c1f0", 191 | "name": "", 192 | "mode": "default", 193 | "component_definitions_are_NOT_allowed_inside_subflows": true, 194 | "x": 770, 195 | "y": 280, 196 | "wires": [] 197 | } 198 | ] -------------------------------------------------------------------------------- /images/component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollixx/node-red-contrib-components/003c06a532bd6b24815d01e79d3b0e8cafb30175/images/component.png -------------------------------------------------------------------------------- /images/component_in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollixx/node-red-contrib-components/003c06a532bd6b24815d01e79d3b0e8cafb30175/images/component_in.png -------------------------------------------------------------------------------- /images/component_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollixx/node-red-contrib-components/003c06a532bd6b24815d01e79d3b0e8cafb30175/images/component_out.png -------------------------------------------------------------------------------- /images/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollixx/node-red-contrib-components/003c06a532bd6b24815d01e79d3b0e8cafb30175/images/components.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-components", 3 | "description": "reusable flows with a well defined API. Write components and use them anywhere in your node-red app", 4 | "version": "0.3.4", 5 | "license": "MIT", 6 | "author": "Oliver Charlet ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ollixx/node-red-contrib-components.git" 10 | }, 11 | "keywords": [ 12 | "node-red", 13 | "component", 14 | "reusable", 15 | "flow", 16 | "api", 17 | "subflow" 18 | ], 19 | "node-red": { 20 | "nodes": { 21 | "component-start": "components/component-start.js", 22 | "component-return": "components/component-return.js", 23 | "run-component": "components/run-component.js" 24 | } 25 | }, 26 | "devDependencies": { 27 | "mocha": "^7.0.1", 28 | "node-red": "^1.2.2", 29 | "node-red-node-test-helper": "^0.2.7", 30 | "puppeteer": "^12.0.1" 31 | }, 32 | "dependencies": {}, 33 | "scripts": { 34 | "test": "mocha components/test/*_spec.js", 35 | "uitest": "npm --prefix ./components/uitest/ install; cd components/uitest/; mocha *_spec.js" 36 | } 37 | } --------------------------------------------------------------------------------