Skip to content

Commit 5752ba6

Browse files
authored
Merge pull request #392
fix(21625): Prevent cycles in the Designer graphs * fix(21625): refactor the isValidConnection routine to detect self-con… * test(21625): add tests * refactor(21625): properly return a value * test(21625): fix test
1 parent 2e700c3 commit 5752ba6

File tree

4 files changed

+125
-22
lines changed

4 files changed

+125
-22
lines changed

hivemq-edge/src/frontend/src/components/Chakra/ShortcutRenderer.spec.cy.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ describe('ShortcutRenderer', () => {
1919

2020
it('should render multiple shortcuts', () => {
2121
cy.mountWithProviders(<ShortcutRenderer hotkeys="CTRL+C,Meta+V,ESC" description="This is a description" />)
22+
const cmd = Cypress.platform === 'darwin' ? 'Command' : 'Ctrl'
23+
console.log('XXXXXX', cmd)
2224

23-
cy.get('[role="term"]').should('contain.text', 'CTRL + C , Command + V , ESC')
25+
cy.get('[role="term"]').should('contain.text', `CTRL + C , ${cmd} + V , ESC`)
2426
cy.get('kbd').should('have.length', 5)
2527
})
2628

hivemq-edge/src/frontend/src/extensions/datahub/components/pages/PolicyEditor.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ const PolicyEditor: FC = () => {
2424

2525
const nodeTypes = useMemo(() => CustomNodeTypes, [])
2626

27-
const checkValidity = useCallback((connection: Connection) => isValidPolicyConnection(connection, nodes), [nodes])
27+
const checkValidity = useCallback(
28+
(connection: Connection) => isValidPolicyConnection(connection, nodes, edges),
29+
[edges, nodes]
30+
)
2831

2932
const onDragOver = useCallback((event: React.DragEvent<HTMLElement> | undefined) => {
3033
if (event) {

hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.spec.ts

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
22
import { DataHubNodeType, DataPolicyData, OperationData, SchemaType, StrategyType, ValidatorType } from '../types.ts'
33
import { getNodeId, getNodePayload, isValidPolicyConnection } from './node.utils.ts'
44
import { MOCK_JSONSCHEMA_SCHEMA } from '../__test-utils__/schema.mocks.ts'
5-
import { Node } from 'reactflow'
5+
import { Edge, Node } from 'reactflow'
66
import { MOCK_DEFAULT_NODE } from '@/__test-utils__/react-flow/nodes.ts'
77

88
describe('getNodeId', () => {
@@ -70,7 +70,7 @@ describe('getNodePayload', () => {
7070
describe('isValidPolicyConnection', () => {
7171
it('should not be a valid connection with an unknown source', async () => {
7272
expect(
73-
isValidPolicyConnection({ source: 'source', target: 'target', sourceHandle: null, targetHandle: null }, [])
73+
isValidPolicyConnection({ source: 'source', target: 'target', sourceHandle: null, targetHandle: null }, [], [])
7474
).toBeFalsy()
7575
})
7676

@@ -82,9 +82,11 @@ describe('isValidPolicyConnection', () => {
8282
position: { x: 0, y: 0 },
8383
}
8484
expect(
85-
isValidPolicyConnection({ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null }, [
86-
MOCK_NODE_DATA_POLICY,
87-
])
85+
isValidPolicyConnection(
86+
{ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null },
87+
[MOCK_NODE_DATA_POLICY],
88+
[]
89+
)
8890
).toBeFalsy()
8991
})
9092

@@ -97,9 +99,11 @@ describe('isValidPolicyConnection', () => {
9799
position: { x: 0, y: 0 },
98100
}
99101
expect(
100-
isValidPolicyConnection({ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null }, [
101-
MOCK_NODE_DATA_POLICY,
102-
])
102+
isValidPolicyConnection(
103+
{ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null },
104+
[MOCK_NODE_DATA_POLICY],
105+
[]
106+
)
103107
).toBeFalsy()
104108
})
105109

@@ -120,10 +124,11 @@ describe('isValidPolicyConnection', () => {
120124
position: { x: 0, y: 0 },
121125
}
122126
expect(
123-
isValidPolicyConnection({ source: 'node-id', target: 'node-operation', sourceHandle: null, targetHandle: null }, [
124-
MOCK_NODE_DATA_POLICY,
125-
MOCK_NODE_OPERATION,
126-
])
127+
isValidPolicyConnection(
128+
{ source: 'node-id', target: 'node-operation', sourceHandle: null, targetHandle: null },
129+
[MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
130+
[]
131+
)
127132
).toBeTruthy()
128133
})
129134

@@ -151,7 +156,8 @@ describe('isValidPolicyConnection', () => {
151156
sourceHandle: null,
152157
targetHandle: OperationData.Handle.SCHEMA,
153158
},
154-
[MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION]
159+
[MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
160+
[]
155161
)
156162
).toBeTruthy()
157163

@@ -163,7 +169,75 @@ describe('isValidPolicyConnection', () => {
163169
sourceHandle: null,
164170
targetHandle: DataPolicyData.Handle.ON_SUCCESS,
165171
},
166-
[MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION]
172+
[MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
173+
[]
174+
)
175+
).toBeFalsy()
176+
})
177+
178+
it('should not be a valid connection if self-referencing', async () => {
179+
const MOCK_NODE_OPERATION: Node = {
180+
id: 'node-operation',
181+
type: DataHubNodeType.OPERATION,
182+
data: {},
183+
...MOCK_DEFAULT_NODE,
184+
position: { x: 0, y: 0 },
185+
}
186+
expect(
187+
isValidPolicyConnection(
188+
{
189+
source: 'node-operation',
190+
target: 'node-operation',
191+
sourceHandle: null,
192+
targetHandle: null,
193+
},
194+
[MOCK_NODE_OPERATION],
195+
[]
196+
)
197+
).toBeFalsy()
198+
})
199+
200+
it('should not be a valid connection if creating a cycle', async () => {
201+
const MOCK_NODE_OPERATION: Node = {
202+
id: 'node-operation',
203+
type: DataHubNodeType.OPERATION,
204+
data: {},
205+
...MOCK_DEFAULT_NODE,
206+
position: { x: 0, y: 0 },
207+
}
208+
209+
const nodes: Node[] = [
210+
MOCK_NODE_OPERATION,
211+
{ ...MOCK_NODE_OPERATION, id: 'node-operation-1' },
212+
{ ...MOCK_NODE_OPERATION, id: 'node-operation-2' },
213+
]
214+
const edges: Edge[] = [
215+
{ id: '1', source: 'node-operation', target: 'node-operation-1', sourceHandle: null, targetHandle: null },
216+
{ id: '1', source: 'node-operation-1', target: 'node-operation-2', sourceHandle: null, targetHandle: null },
217+
]
218+
219+
expect(
220+
isValidPolicyConnection(
221+
{
222+
source: 'node-operation-1',
223+
target: 'node-operation',
224+
sourceHandle: null,
225+
targetHandle: null,
226+
},
227+
nodes,
228+
edges
229+
)
230+
).toBeFalsy()
231+
expect(
232+
isValidPolicyConnection(
233+
{
234+
source: 'node-operation-2',
235+
target: 'node-operation',
236+
sourceHandle: null,
237+
targetHandle: null,
238+
},
239+
nodes,
240+
edges
167241
)
168242
).toBeFalsy()
169243
})

hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Connection, Node } from 'reactflow'
1+
import { Connection, Edge, getOutgoers, Node } from 'reactflow'
22
import { v4 as uuidv4 } from 'uuid'
33
import { MOCK_JSONSCHEMA_SCHEMA } from '../__test-utils__/schema.mocks.ts'
44

@@ -78,18 +78,42 @@ export const validConnections: ConnectionValidity = {
7878
[DataHubNodeType.FUNCTION]: [[DataHubNodeType.OPERATION, OperationData.Handle.FUNCTION]],
7979
}
8080

81-
export const isValidPolicyConnection = (connection: Connection, nodes: Node[]) => {
81+
export const isValidPolicyConnection = (connection: Connection, nodes: Node[], edges: Edge[]) => {
8282
const source = nodes.find((node) => node.id === connection.source)
8383
const destination = nodes.find((node) => node.id === connection.target)
8484

85-
if (!source) {
85+
const hasCycle = (node: Node, visited = new Set()) => {
86+
if (visited.has(node.id)) return false
87+
88+
visited.add(node.id)
89+
90+
for (const outgoer of getOutgoers(node, nodes, edges)) {
91+
if (outgoer.id === connection.source) return true
92+
if (hasCycle(outgoer, visited)) return true
93+
}
94+
return false
95+
}
96+
97+
if (!source || !destination) {
98+
return false
99+
}
100+
101+
if (!source.type) {
102+
// node that are not Data Hub types are illegal
86103
return false
87104
}
88-
const { type } = source
89-
if (!type) {
105+
106+
if (destination.id === source.id) {
107+
// self-connection are illegal
90108
return false
91109
}
92-
const connectionValidators = validConnections[type]
110+
111+
if (hasCycle(destination)) {
112+
// cycle are illegal
113+
return false
114+
}
115+
116+
const connectionValidators = validConnections[source.type]
93117
if (!connectionValidators) {
94118
return false
95119
}

0 commit comments

Comments
 (0)