Skip to content

Commit 92de9dc

Browse files
committed
#58 Harden component return validation for junctions
1 parent ac62e0f commit 92de9dc

3 files changed

Lines changed: 186 additions & 16 deletions

File tree

components/component-return.html

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,54 @@
88
const MSG_NOT_CONNECTED = "notConnected";
99
const MSG_TOO_MANY_START_NODES = "tooManyStartNodes";
1010

11-
const findStartNodes = function (nodeId, sources = {}, visited = [], found = []) {
12-
if (visited.includes(nodeId)) {
11+
const getEndpointId = function (endpoint) {
12+
if (!endpoint) {
13+
return null;
14+
}
15+
if (typeof endpoint === "string") {
16+
return endpoint;
17+
}
18+
return endpoint.id || null;
19+
};
20+
21+
const findStartNodes = function (nodeId, visited = new Set(), found = new Set()) {
22+
if (!nodeId || visited.has(nodeId)) {
1323
return found;
1424
}
15-
visited.push(nodeId);
16-
let node = RED.nodes.node(nodeId);
25+
26+
visited.add(nodeId);
27+
const node = RED.nodes.node(nodeId);
1728
if (!node) {
1829
return found;
1930
}
20-
if (node.type == "component_in") {
21-
found.push(nodeId);
31+
32+
if (node.type === "component_in") {
33+
found.add(nodeId);
2234
}
35+
2336
RED.nodes.eachLink((link) => {
24-
if (link.target.id == nodeId) {
25-
sources[link.source.id] = {};
26-
findStartNodes(link.source.id, sources[link.source.id], visited, found);
37+
if (getEndpointId(link && link.target) !== nodeId) {
38+
return;
2739
}
28-
});
29-
if (node.type == "link in" && node.links) {
30-
for (let lout of node.links) {
31-
sources[lout] = {};
32-
findStartNodes(lout, sources[lout], visited, found);
40+
41+
const sourceId = getEndpointId(link && link.source);
42+
if (sourceId) {
43+
findStartNodes(sourceId, visited, found);
3344
}
45+
});
46+
47+
if (node.type === "link in" && Array.isArray(node.links)) {
48+
node.links.forEach((linkOutId) => {
49+
findStartNodes(linkOutId, visited, found);
50+
});
3451
}
52+
3553
return found;
3654
};
3755

3856
const getValidationResult = function (node) {
39-
let startNodes = findStartNodes(node.id);
40-
if (startNodes.length == 0) {
57+
const startNodes = Array.from(findStartNodes(node && node.id));
58+
if (startNodes.length === 0) {
4159
return {
4260
codes: [MSG_NOT_CONNECTED],
4361
message: RED._("components.message.componentNotConnected")

components/lib/editor-graph.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
function getEndpointId(endpoint) {
2+
if (!endpoint) {
3+
return null;
4+
}
5+
6+
if (typeof endpoint === "string") {
7+
return endpoint;
8+
}
9+
10+
return endpoint.id || null;
11+
}
12+
13+
function findStartNodes(RED, nodeId, visited = new Set(), found = new Set()) {
14+
if (!nodeId || visited.has(nodeId)) {
15+
return found;
16+
}
17+
18+
visited.add(nodeId);
19+
20+
const node = RED.nodes.node(nodeId);
21+
if (!node) {
22+
return found;
23+
}
24+
25+
if (node.type === "component_in") {
26+
found.add(nodeId);
27+
}
28+
29+
RED.nodes.eachLink((link) => {
30+
if (getEndpointId(link && link.target) !== nodeId) {
31+
return;
32+
}
33+
34+
const sourceId = getEndpointId(link && link.source);
35+
if (sourceId) {
36+
findStartNodes(RED, sourceId, visited, found);
37+
}
38+
});
39+
40+
if (node.type === "link in" && Array.isArray(node.links)) {
41+
node.links.forEach((linkOutId) => {
42+
findStartNodes(RED, linkOutId, visited, found);
43+
});
44+
}
45+
46+
return found;
47+
}
48+
49+
function getComponentReturnValidationResult(RED, node) {
50+
const startNodes = Array.from(findStartNodes(RED, node && node.id));
51+
if (startNodes.length === 0) {
52+
return {
53+
codes: ["notConnected"],
54+
message: RED._("components.message.componentNotConnected")
55+
};
56+
}
57+
58+
if (startNodes.length > 1) {
59+
return {
60+
codes: ["tooManyStartNodes"],
61+
message: RED._("components.message.returnWithoutStart", { inNodeLength: startNodes.length })
62+
};
63+
}
64+
65+
return { codes: [], message: "" };
66+
}
67+
68+
module.exports = {
69+
findStartNodes,
70+
getComponentReturnValidationResult
71+
};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
var should = require("should");
2+
var editorGraph = require("../lib/editor-graph");
3+
4+
function createEditorRed(nodes, links) {
5+
return {
6+
_: function (key, args) {
7+
if (key === "components.message.returnWithoutStart") {
8+
return key + ":" + args.inNodeLength;
9+
}
10+
return key;
11+
},
12+
nodes: {
13+
node: function (id) {
14+
return nodes[id] || null;
15+
},
16+
eachLink: function (callback) {
17+
links.forEach(callback);
18+
}
19+
}
20+
};
21+
}
22+
23+
describe("editor graph helpers", function () {
24+
it("should validate component_out through junction links", function () {
25+
var red = createEditorRed(
26+
{
27+
in01: { id: "in01", type: "component_in" },
28+
junction01: { id: "junction01", type: "junction" },
29+
ret01: { id: "ret01", type: "component_out" }
30+
},
31+
[
32+
{ source: { id: "in01" }, target: { id: "junction01" } },
33+
{ source: { id: "junction01" }, target: { id: "ret01" } }
34+
]
35+
);
36+
37+
editorGraph.getComponentReturnValidationResult(red, { id: "ret01" }).should.eql({
38+
codes: [],
39+
message: ""
40+
});
41+
});
42+
43+
it("should ignore malformed links while finding start nodes", function () {
44+
var red = createEditorRed(
45+
{
46+
in01: { id: "in01", type: "component_in" },
47+
junction01: { id: "junction01", type: "junction" },
48+
ret01: { id: "ret01", type: "component_out" }
49+
},
50+
[
51+
{ source: { id: "in01" }, target: { id: "junction01" } },
52+
{ source: "junction01", target: "ret01" },
53+
{ source: undefined, target: { id: "ret01" } },
54+
{ source: { id: "missing" }, target: null }
55+
]
56+
);
57+
58+
Array.from(editorGraph.findStartNodes(red, "ret01")).should.eql(["in01"]);
59+
});
60+
61+
it("should report multiple component starts as invalid", function () {
62+
var red = createEditorRed(
63+
{
64+
in01: { id: "in01", type: "component_in" },
65+
in02: { id: "in02", type: "component_in" },
66+
junction01: { id: "junction01", type: "junction" },
67+
ret01: { id: "ret01", type: "component_out" }
68+
},
69+
[
70+
{ source: { id: "in01" }, target: { id: "junction01" } },
71+
{ source: { id: "in02" }, target: { id: "junction01" } },
72+
{ source: { id: "junction01" }, target: { id: "ret01" } }
73+
]
74+
);
75+
76+
editorGraph.getComponentReturnValidationResult(red, { id: "ret01" }).should.eql({
77+
codes: ["tooManyStartNodes"],
78+
message: "components.message.returnWithoutStart:2"
79+
});
80+
});
81+
});

0 commit comments

Comments
 (0)