├── .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 | 
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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------