@k-4u/resource-mapper-core 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/coverage/junit.xml +17 -17
  2. package/coverage/test-results.json +1 -1
  3. package/data/services/api/api-gateway.yaml +18 -18
  4. package/data/services/api/group-info.yaml +7 -7
  5. package/data/services/api/lambda-orders.yaml +21 -21
  6. package/data/services/api/lambda-products.yaml +15 -15
  7. package/data/services/api/lambda-users.yaml +15 -15
  8. package/data/services/compute/alb.yaml +15 -15
  9. package/data/services/compute/ecs-inventory.yaml +16 -16
  10. package/data/services/compute/ecs-notification.yaml +15 -15
  11. package/data/services/compute/group-info.yaml +7 -7
  12. package/data/services/data/dynamodb-notifications.yaml +12 -12
  13. package/data/services/data/dynamodb-orders.yaml +9 -9
  14. package/data/services/data/dynamodb-products.yaml +9 -9
  15. package/data/services/data/dynamodb-users.yaml +9 -9
  16. package/data/services/data/group-info.yaml +7 -7
  17. package/data/services/data/rds-postgres.yaml +9 -9
  18. package/data/services/data/redis.yaml +10 -10
  19. package/data/services/frontend/cloudfront.yaml +12 -12
  20. package/data/services/frontend/group-info.yaml +7 -7
  21. package/data/services/frontend/route53.yaml +15 -15
  22. package/data/services/frontend/s3-website.yaml +9 -9
  23. package/data/teams/cloud-shepherds.yaml +15 -15
  24. package/data/teams/data-wizards.yaml +15 -15
  25. package/data/teams/interface-architects.yaml +18 -18
  26. package/e2e/demo.test.ts +54 -54
  27. package/e2e/header-toolbar.spec.ts +53 -53
  28. package/e2e/layout.spec.ts +30 -30
  29. package/package.json +74 -69
  30. package/playwright.config.ts +10 -10
  31. package/plugins/mapper-data-plugin.ts +32 -32
  32. package/project.json +22 -22
  33. package/src/app.css +125 -125
  34. package/src/app.d.ts +31 -31
  35. package/src/app.html +11 -11
  36. package/src/lib/assets/favicon.svg +18 -18
  37. package/src/lib/components/EmptyState.svelte +37 -37
  38. package/src/lib/components/ErrorDisplay.svelte +82 -82
  39. package/src/lib/components/FlowCanvas.svelte +223 -223
  40. package/src/lib/components/GenericSidebarCard.svelte +43 -43
  41. package/src/lib/components/GroupDetailSidebar.svelte +31 -31
  42. package/src/lib/components/Header.svelte +57 -57
  43. package/src/lib/components/Legend.svelte +25 -25
  44. package/src/lib/components/LoadingOverlay.svelte +42 -42
  45. package/src/lib/components/LoadingSpinner.svelte +10 -10
  46. package/src/lib/components/ServiceDetailSidebar.svelte +89 -89
  47. package/src/lib/components/TeamContactCard.svelte +166 -166
  48. package/src/lib/components/flow/ExternalNode.svelte +45 -45
  49. package/src/lib/components/flow/MainGroupNode.svelte +24 -24
  50. package/src/lib/components/flow/ServiceGroupNode.svelte +17 -17
  51. package/src/lib/components/flow/ServiceNode.svelte +40 -40
  52. package/src/lib/components/flow/SnakeEdge.svelte +206 -206
  53. package/src/lib/components/flow/index.ts +5 -5
  54. package/src/lib/components/index.ts +12 -12
  55. package/src/lib/data/connections.ts +26 -26
  56. package/src/lib/data/groups.ts +11 -11
  57. package/src/lib/data/services.ts +12 -12
  58. package/src/lib/data/teams.ts +11 -11
  59. package/src/lib/index.ts +1 -1
  60. package/src/lib/state/theme.svelte.ts +21 -21
  61. package/src/lib/stores/diagram.ts +6 -6
  62. package/src/lib/stores/routingStore.test.ts +133 -133
  63. package/src/lib/stores/routingStore.ts +232 -232
  64. package/src/lib/utils/awsIcons.ts +117 -117
  65. package/src/lib/utils/flow/groupOverviewGraph.ts +73 -73
  66. package/src/lib/utils/flow/helpers.ts +14 -14
  67. package/src/lib/utils/flow/layout.test.ts +271 -271
  68. package/src/lib/utils/flow/layout.ts +240 -240
  69. package/src/lib/utils/flow/serviceIds.ts +4 -4
  70. package/src/lib/utils/flow/servicesGraph.test.ts +119 -119
  71. package/src/lib/utils/flow/servicesGraph.ts +258 -258
  72. package/src/routes/+error.svelte +36 -36
  73. package/src/routes/+layout.svelte +27 -27
  74. package/src/routes/+page.svelte +81 -81
  75. package/src/routes/+page.ts +31 -31
  76. package/src/routes/group/[groupId]/+page.svelte +102 -102
  77. package/src/routes/group/[groupId]/+page.ts +48 -48
  78. package/static/static/robots.txt +3 -3
  79. package/svelte.config.js +27 -27
  80. package/tailwind.config.js +12 -12
  81. package/tsconfig.json +22 -22
  82. package/vite.config.ts +80 -80
  83. package/.aws-icons-last-updated +0 -1
  84. package/.svelte-kit/ambient.d.ts +0 -263
  85. package/.svelte-kit/generated/client/app.js +0 -31
  86. package/.svelte-kit/generated/client/matchers.js +0 -1
  87. package/.svelte-kit/generated/client/nodes/0.js +0 -1
  88. package/.svelte-kit/generated/client/nodes/1.js +0 -1
  89. package/.svelte-kit/generated/client/nodes/2.js +0 -3
  90. package/.svelte-kit/generated/client/nodes/3.js +0 -3
  91. package/.svelte-kit/generated/client-optimized/app.js +0 -31
  92. package/.svelte-kit/generated/client-optimized/matchers.js +0 -1
  93. package/.svelte-kit/generated/client-optimized/nodes/0.js +0 -1
  94. package/.svelte-kit/generated/client-optimized/nodes/1.js +0 -1
  95. package/.svelte-kit/generated/client-optimized/nodes/2.js +0 -3
  96. package/.svelte-kit/generated/client-optimized/nodes/3.js +0 -3
  97. package/.svelte-kit/generated/root.js +0 -3
  98. package/.svelte-kit/generated/root.svelte +0 -68
  99. package/.svelte-kit/generated/server/internal.js +0 -53
  100. package/.svelte-kit/non-ambient.d.ts +0 -43
  101. package/.svelte-kit/output/client/.vite/manifest.json +0 -175
  102. package/.svelte-kit/output/client/_app/immutable/assets/0.Czt_67iE.css +0 -1
  103. package/.svelte-kit/output/client/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +0 -1
  104. package/.svelte-kit/output/client/_app/immutable/assets/helpers.ysDrpaDf.css +0 -1
  105. package/.svelte-kit/output/client/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
  106. package/.svelte-kit/output/client/_app/immutable/chunks/BlLuv0eP.js +0 -46
  107. package/.svelte-kit/output/client/_app/immutable/chunks/CSBHmwYv.js +0 -1
  108. package/.svelte-kit/output/client/_app/immutable/chunks/CTCi5ueQ.js +0 -1
  109. package/.svelte-kit/output/client/_app/immutable/chunks/CfOzjaik.js +0 -2
  110. package/.svelte-kit/output/client/_app/immutable/chunks/D4PdvFNs.js +0 -1
  111. package/.svelte-kit/output/client/_app/immutable/chunks/DXgP-QUS.js +0 -2
  112. package/.svelte-kit/output/client/_app/immutable/chunks/DlbDC5An.js +0 -1
  113. package/.svelte-kit/output/client/_app/immutable/chunks/wRWe7aK9.js +0 -1
  114. package/.svelte-kit/output/client/_app/immutable/entry/app.ConrMuHl.js +0 -2
  115. package/.svelte-kit/output/client/_app/immutable/entry/start.Bm6FyGme.js +0 -1
  116. package/.svelte-kit/output/client/_app/immutable/nodes/0.d3cL-ETU.js +0 -1
  117. package/.svelte-kit/output/client/_app/immutable/nodes/1.D6z9rPGv.js +0 -1
  118. package/.svelte-kit/output/client/_app/immutable/nodes/2.CLD-8chl.js +0 -1
  119. package/.svelte-kit/output/client/_app/immutable/nodes/3.DXYeBoel.js +0 -1
  120. package/.svelte-kit/output/client/_app/version.json +0 -1
  121. package/.svelte-kit/output/client/libavoid.wasm +0 -0
  122. package/.svelte-kit/output/client/static/robots.txt +0 -3
  123. package/.svelte-kit/output/prerendered/dependencies/_app/env.js +0 -1
  124. package/.svelte-kit/output/server/.vite/manifest.json +0 -224
  125. package/.svelte-kit/output/server/_app/immutable/assets/LoadingOverlay.DBbe6V8W.css +0 -1
  126. package/.svelte-kit/output/server/_app/immutable/assets/_layout.Czt_67iE.css +0 -1
  127. package/.svelte-kit/output/server/_app/immutable/assets/_page.D9P41uDZ.css +0 -1
  128. package/.svelte-kit/output/server/chunks/ErrorDisplay.js +0 -59
  129. package/.svelte-kit/output/server/chunks/LoadingOverlay.js +0 -12
  130. package/.svelte-kit/output/server/chunks/LoadingOverlay.svelte_svelte_type_style_lang.js +0 -1671
  131. package/.svelte-kit/output/server/chunks/connections.js +0 -33
  132. package/.svelte-kit/output/server/chunks/diagram.js +0 -7
  133. package/.svelte-kit/output/server/chunks/environment.js +0 -34
  134. package/.svelte-kit/output/server/chunks/equality.js +0 -57
  135. package/.svelte-kit/output/server/chunks/exports.js +0 -174
  136. package/.svelte-kit/output/server/chunks/false.js +0 -4
  137. package/.svelte-kit/output/server/chunks/index.js +0 -59
  138. package/.svelte-kit/output/server/chunks/index2.js +0 -2939
  139. package/.svelte-kit/output/server/chunks/index3.js +0 -20
  140. package/.svelte-kit/output/server/chunks/internal.js +0 -1017
  141. package/.svelte-kit/output/server/chunks/shared.js +0 -770
  142. package/.svelte-kit/output/server/chunks/utils.js +0 -43
  143. package/.svelte-kit/output/server/entries/pages/_error.svelte.js +0 -64
  144. package/.svelte-kit/output/server/entries/pages/_layout.svelte.js +0 -65
  145. package/.svelte-kit/output/server/entries/pages/_page.svelte.js +0 -3991
  146. package/.svelte-kit/output/server/entries/pages/_page.ts.js +0 -30
  147. package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.svelte.js +0 -67
  148. package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.ts.js +0 -47
  149. package/.svelte-kit/output/server/index.js +0 -3747
  150. package/.svelte-kit/output/server/internal.js +0 -13
  151. package/.svelte-kit/output/server/manifest-full.js +0 -47
  152. package/.svelte-kit/output/server/manifest.js +0 -47
  153. package/.svelte-kit/output/server/nodes/0.js +0 -8
  154. package/.svelte-kit/output/server/nodes/1.js +0 -8
  155. package/.svelte-kit/output/server/nodes/2.js +0 -10
  156. package/.svelte-kit/output/server/nodes/3.js +0 -10
  157. package/.svelte-kit/output/server/remote-entry.js +0 -557
  158. package/.svelte-kit/tsconfig.json +0 -61
  159. package/.svelte-kit/types/route_meta_data.json +0 -8
  160. package/.svelte-kit/types/src/routes/$types.d.ts +0 -26
  161. package/.svelte-kit/types/src/routes/group/[groupId]/$types.d.ts +0 -21
  162. package/.svelte-kit/types/src/routes/group/[groupId]/proxy+page.ts +0 -49
  163. package/.svelte-kit/types/src/routes/proxy+page.ts +0 -33
  164. package/build/_app/env.js +0 -1
  165. package/build/_app/env.js.br +0 -1
  166. package/build/_app/env.js.gz +0 -0
  167. package/build/_app/immutable/assets/0.Czt_67iE.css +0 -1
  168. package/build/_app/immutable/assets/0.Czt_67iE.css.br +0 -0
  169. package/build/_app/immutable/assets/0.Czt_67iE.css.gz +0 -0
  170. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +0 -1
  171. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.br +0 -0
  172. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.gz +0 -0
  173. package/build/_app/immutable/assets/helpers.ysDrpaDf.css +0 -1
  174. package/build/_app/immutable/assets/helpers.ysDrpaDf.css.br +0 -0
  175. package/build/_app/immutable/assets/helpers.ysDrpaDf.css.gz +0 -0
  176. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
  177. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.br +0 -0
  178. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.gz +0 -0
  179. package/build/_app/immutable/chunks/BlLuv0eP.js +0 -46
  180. package/build/_app/immutable/chunks/BlLuv0eP.js.br +0 -0
  181. package/build/_app/immutable/chunks/BlLuv0eP.js.gz +0 -0
  182. package/build/_app/immutable/chunks/CSBHmwYv.js +0 -1
  183. package/build/_app/immutable/chunks/CSBHmwYv.js.br +0 -0
  184. package/build/_app/immutable/chunks/CSBHmwYv.js.gz +0 -0
  185. package/build/_app/immutable/chunks/CTCi5ueQ.js +0 -1
  186. package/build/_app/immutable/chunks/CTCi5ueQ.js.br +0 -0
  187. package/build/_app/immutable/chunks/CTCi5ueQ.js.gz +0 -0
  188. package/build/_app/immutable/chunks/CfOzjaik.js +0 -2
  189. package/build/_app/immutable/chunks/CfOzjaik.js.br +0 -0
  190. package/build/_app/immutable/chunks/CfOzjaik.js.gz +0 -0
  191. package/build/_app/immutable/chunks/D4PdvFNs.js +0 -1
  192. package/build/_app/immutable/chunks/D4PdvFNs.js.br +0 -0
  193. package/build/_app/immutable/chunks/D4PdvFNs.js.gz +0 -0
  194. package/build/_app/immutable/chunks/DXgP-QUS.js +0 -2
  195. package/build/_app/immutable/chunks/DXgP-QUS.js.br +0 -0
  196. package/build/_app/immutable/chunks/DXgP-QUS.js.gz +0 -0
  197. package/build/_app/immutable/chunks/DlbDC5An.js +0 -1
  198. package/build/_app/immutable/chunks/DlbDC5An.js.br +0 -0
  199. package/build/_app/immutable/chunks/DlbDC5An.js.gz +0 -0
  200. package/build/_app/immutable/chunks/wRWe7aK9.js +0 -1
  201. package/build/_app/immutable/chunks/wRWe7aK9.js.br +0 -0
  202. package/build/_app/immutable/chunks/wRWe7aK9.js.gz +0 -0
  203. package/build/_app/immutable/entry/app.ConrMuHl.js +0 -2
  204. package/build/_app/immutable/entry/app.ConrMuHl.js.br +0 -0
  205. package/build/_app/immutable/entry/app.ConrMuHl.js.gz +0 -0
  206. package/build/_app/immutable/entry/start.Bm6FyGme.js +0 -1
  207. package/build/_app/immutable/entry/start.Bm6FyGme.js.br +0 -2
  208. package/build/_app/immutable/entry/start.Bm6FyGme.js.gz +0 -0
  209. package/build/_app/immutable/nodes/0.d3cL-ETU.js +0 -1
  210. package/build/_app/immutable/nodes/0.d3cL-ETU.js.br +0 -0
  211. package/build/_app/immutable/nodes/0.d3cL-ETU.js.gz +0 -0
  212. package/build/_app/immutable/nodes/1.D6z9rPGv.js +0 -1
  213. package/build/_app/immutable/nodes/1.D6z9rPGv.js.br +0 -0
  214. package/build/_app/immutable/nodes/1.D6z9rPGv.js.gz +0 -0
  215. package/build/_app/immutable/nodes/2.CLD-8chl.js +0 -1
  216. package/build/_app/immutable/nodes/2.CLD-8chl.js.br +0 -0
  217. package/build/_app/immutable/nodes/2.CLD-8chl.js.gz +0 -0
  218. package/build/_app/immutable/nodes/3.DXYeBoel.js +0 -1
  219. package/build/_app/immutable/nodes/3.DXYeBoel.js.br +0 -0
  220. package/build/_app/immutable/nodes/3.DXYeBoel.js.gz +0 -0
  221. package/build/_app/version.json +0 -1
  222. package/build/_app/version.json.br +0 -0
  223. package/build/_app/version.json.gz +0 -0
  224. package/build/index.html +0 -34
  225. package/build/index.html.br +0 -0
  226. package/build/index.html.gz +0 -0
  227. package/build/libavoid.wasm +0 -0
  228. package/build/libavoid.wasm.br +0 -0
  229. package/build/libavoid.wasm.gz +0 -0
  230. package/build/static/robots.txt +0 -3
  231. package/coverage/coverage-final.json +0 -6
  232. package/coverage/coverage-summary.json +0 -7
  233. package/coverage/lcov-report/base.css +0 -224
  234. package/coverage/lcov-report/block-navigation.js +0 -87
  235. package/coverage/lcov-report/favicon.png +0 -0
  236. package/coverage/lcov-report/index.html +0 -131
  237. package/coverage/lcov-report/prettify.css +0 -1
  238. package/coverage/lcov-report/prettify.js +0 -2
  239. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  240. package/coverage/lcov-report/sorter.js +0 -210
  241. package/coverage/lcov-report/stores/index.html +0 -116
  242. package/coverage/lcov-report/stores/routingStore.ts.html +0 -781
  243. package/coverage/lcov-report/utils/flow/helpers.ts.html +0 -127
  244. package/coverage/lcov-report/utils/flow/index.html +0 -161
  245. package/coverage/lcov-report/utils/flow/layout.ts.html +0 -805
  246. package/coverage/lcov-report/utils/flow/serviceIds.ts.html +0 -97
  247. package/coverage/lcov-report/utils/flow/servicesGraph.ts.html +0 -859
  248. package/coverage/lcov.info +0 -646
@@ -1,271 +1,271 @@
1
- import {describe, it, expect} from 'vitest'
2
- import type {Edge, Node} from '@xyflow/svelte'
3
- import {layoutFlowGraph, calculateEdgeOffset, OFFSET_STEP} from './layout'
4
- import type {FlowEdgeData, FlowGraphInput, FlowNodeData} from '$shared/flow-types'
5
-
6
- // Basic helpers to build nodes/edges with minimal required data
7
- function serviceNode(id: string, label = id, serviceType?: string, position = {x: 0, y: 0}): Node<FlowNodeData> {
8
- return {
9
- id,
10
- data: {label, kind: 'service', serviceType},
11
- position,
12
- type: 'service'
13
- } as unknown as Node<FlowNodeData>
14
- }
15
-
16
- function groupNode(id: string, label = id, position = {x: 0, y: 0}): Node<FlowNodeData> {
17
- return {
18
- id,
19
- data: {label, kind: 'group'},
20
- position,
21
- type: 'serviceGroup'
22
- } as unknown as Node<FlowNodeData>
23
- }
24
-
25
- function edge(id: string, source: string, target: string, data: FlowEdgeData = {}): Edge<FlowEdgeData> {
26
- return {
27
- id,
28
- source,
29
- target,
30
- data
31
- } as Edge<FlowEdgeData>
32
- }
33
-
34
- // Silence verbose debug logs during tests
35
- // eslint-disable-next-line @typescript-eslint/no-empty-function
36
- const noop = () => {
37
- }
38
- // @ts-expect-error allow override in test env
39
- console.debug = noop
40
- // @ts-expect-error allow override in test env
41
- console.log = noop
42
-
43
- describe('layoutFlowGraph', () => {
44
- it('returns positions for nodes and preserves signature', async () => {
45
- const input: FlowGraphInput = {
46
- groupNodes: [],
47
- serviceNodes: [serviceNode('s1', 'Service 1')],
48
- edges: [],
49
- signature: 'sig-1'
50
- }
51
-
52
- const out = await layoutFlowGraph(input)
53
-
54
- expect(out.signature).toBe(input.signature)
55
- expect(out.nodes.length).toBeGreaterThanOrEqual(1)
56
-
57
- const n = out.nodes.find((x) => x.id === 's1')
58
- expect(n).toBeTruthy()
59
- expect(n?.position?.x).toBeDefined()
60
- expect(n?.position?.y).toBeDefined()
61
- })
62
-
63
- it('keeps nodes and edges ids and data', async () => {
64
- const input: FlowGraphInput = {
65
- groupNodes: [],
66
- serviceNodes: [serviceNode('a', 'Service A', 'lambda'), serviceNode('b', 'Service B', 'sqs')],
67
- edges: [edge('e1', 'a', 'b', {label: 'A->B', direction: 'outgoing'})],
68
- signature: 'sig-2'
69
- }
70
-
71
- const out = await layoutFlowGraph(input)
72
-
73
- // Nodes should be present
74
- const nodeA = out.nodes.find((n) => n.id === 'a')
75
- const nodeB = out.nodes.find((n) => n.id === 'b')
76
- expect(nodeA).toBeTruthy()
77
- expect(nodeB).toBeTruthy()
78
-
79
- // Verify component-specific data is preserved
80
- expect(nodeA?.data.label).toBe('Service A')
81
- expect(nodeA?.data.serviceType).toBe('lambda')
82
- expect(nodeB?.data.serviceType).toBe('sqs')
83
-
84
- // Edge should be preserved with same ids and data
85
- expect(out.edges.length).toBe(1)
86
- const e = out.edges[0]
87
- expect(e.id).toBe('e1')
88
- expect(e.source).toBe('a')
89
- expect(e.target).toBe('b')
90
- expect(e.data?.label).toBe('A->B')
91
- expect(e.data?.direction).toBe('outgoing')
92
- })
93
-
94
- it('handles a simple group with a child service node', async () => {
95
- const g = groupNode('g1', 'Group 1')
96
- const s = {...serviceNode('s1', 'Service in group'), parentId: 'g1'} as unknown as Node<FlowNodeData>
97
-
98
- const input: FlowGraphInput = {
99
- groupNodes: [g],
100
- serviceNodes: [s],
101
- edges: [],
102
- signature: 'sig-3'
103
- }
104
-
105
- const out = await layoutFlowGraph(input)
106
-
107
- // Expect both the group and the service node to be present in flattened output
108
- const groupOut = out.nodes.find((n) => n.id === 'g1')
109
- const childOut = out.nodes.find((n) => n.id === 's1')
110
-
111
- expect(groupOut).toBeTruthy()
112
- expect(childOut).toBeTruthy()
113
-
114
- // Both should have positions
115
- expect(groupOut?.position?.x).toBeDefined()
116
- expect(groupOut?.position?.y).toBeDefined()
117
- expect(childOut?.position?.x).toBeDefined()
118
- expect(childOut?.position?.y).toBeDefined()
119
- })
120
- })
121
-
122
- describe('calculateEdgeOffset', () => {
123
- it('returns 0 for a single edge', () => {
124
- const nodes: Node<FlowNodeData>[] = [serviceNode('s1'), serviceNode('t1')]
125
- const edges: Edge<FlowEdgeData>[] = [
126
- {id: 'e1', source: 's1', target: 't1', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
127
- ]
128
-
129
- expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(0)
130
- expect(calculateEdgeOffset('e1', nodes, edges, false)).toBe(0)
131
- })
132
-
133
- it('spreads multiple edges and sorts them by target position', () => {
134
- const nodes: Node<FlowNodeData>[] = [
135
- serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
136
- serviceNode('t1', 'Top Target', undefined, {x: 200, y: 0}),
137
- serviceNode('t2', 'Bottom Target', undefined, {x: 200, y: 200})
138
- ]
139
- const edges: Edge<FlowEdgeData>[] = [
140
- {id: 'e_to_t1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
141
- {id: 'e_to_t2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>
142
- ]
143
-
144
- // e_to_t1 connects to t1 (y=0)
145
- // e_to_t2 connects to t2 (y=200)
146
- // Sorted order: e_to_t1, e_to_t2
147
- // index for e_to_t1 = 0. index for e_to_t2 = 1.
148
- // offsets = (index - 0.5) * OFFSET_STEP
149
-
150
- expect(calculateEdgeOffset('e_to_t1', nodes, edges, true)).toBe(-OFFSET_STEP / 2)
151
- expect(calculateEdgeOffset('e_to_t2', nodes, edges, true)).toBe(OFFSET_STEP / 2)
152
- })
153
-
154
- it('spreads three edges correctly', () => {
155
- const nodes: Node<FlowNodeData>[] = [
156
- serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
157
- serviceNode('t1', 'T1', undefined, {x: 200, y: 0}),
158
- serviceNode('t2', 'T2', undefined, {x: 200, y: 100}),
159
- serviceNode('t3', 'T3', undefined, {x: 200, y: 200})
160
- ]
161
- const edges: Edge<FlowEdgeData>[] = [
162
- {id: 'e1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
163
- {id: 'e2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>,
164
- {id: 'e3', source: 's1', target: 't3', sourceHandle: 'output'} as Edge<FlowEdgeData>
165
- ]
166
-
167
- // Sorted order: e1 (y=0), e2 (y=100), e3 (y=200)
168
- // index: 0, 1, 2. (n-1)/2 = 1.
169
- // offsets: (0-1)*OFFSET_STEP, (1-1)*OFFSET_STEP, (2-1)*OFFSET_STEP
170
-
171
- expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(-OFFSET_STEP)
172
- expect(calculateEdgeOffset('e2', nodes, edges, true)).toBe(0)
173
- expect(calculateEdgeOffset('e3', nodes, edges, true)).toBe(OFFSET_STEP)
174
- })
175
-
176
- it('correctly calculates offsets for nodes in groups vs orphans using absolute coordinates', () => {
177
- // Node 1 (orphan) at x=10, y=60
178
- // Group 1 at x=422, y=67
179
- // Node 2 (child of Group 1) at x=10, y=160 (relative) -> Abs: y = 67 + 160 = 227
180
- // Node 3 (child of Group 1) at x=350, y=260 (relative) -> Abs: y = 67 + 260 = 327
181
- // Edges: Node 1 -> Node 3 (e1), Node 2 -> Node 3 (e2)
182
- // Both connect to Node 3's input handle (target).
183
- const nodes: Node<FlowNodeData>[] = [
184
- serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
185
- groupNode('g1', 'Group 1', {x: 422, y: 67}),
186
- {...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
187
- {...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
188
- ]
189
- const edges: Edge<FlowEdgeData>[] = [
190
- {id: 'e1', source: 'n1', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>,
191
- {id: 'e2', source: 'n2', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>
192
- ]
193
-
194
- // Absolute positions:
195
- // n1 (abs y=60)
196
- // n2 (abs y=67+160=227)
197
- // Both are sources of Node 3.
198
- // Order should be e1 (60), e2 (227).
199
- // Offset for e1: -4, Offset for e2: 4.
200
- // This confirms n1's edge is "above" n2's edge (smaller Y offset).
201
- // The user said: "verify if the connection coming from the second node is above of the connection coming from the first node."
202
- // Maybe the user meant if it WAS above? No, I'll just check if e1 is above e2.
203
-
204
- const offset1 = calculateEdgeOffset('e1', nodes, edges, false) // target offset for e1
205
- const offset2 = calculateEdgeOffset('e2', nodes, edges, false) // target offset for e2
206
-
207
- // Expected: offset1 < offset2 because n1 (abs 60) < n2 (abs 227)
208
- expect(offset1).toBe(-OFFSET_STEP / 2)
209
- expect(offset2).toBe(OFFSET_STEP / 2)
210
-
211
- // Now if the parent was at y=-200
212
- // n2 (abs y=-200+160=-40)
213
- // n1 (abs y=60)
214
- // Now n2 is HIGHER than n1.
215
- // Order should be e2 (-40), e1 (60).
216
- // Offset for e2: -4, Offset for e1: 4.
217
- const nodes2: Node<FlowNodeData>[] = [
218
- serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
219
- groupNode('g1', 'Group 1', {x: 422, y: -200}),
220
- {...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
221
- {...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
222
- ]
223
-
224
- const offset1_neg = calculateEdgeOffset('e1', nodes2, edges, false)
225
- const offset2_neg = calculateEdgeOffset('e2', nodes2, edges, false)
226
-
227
- // Expected: offset2_neg < offset1_neg because n2 (abs -40) < n1 (abs 60)
228
- expect(offset2_neg).toBe(-OFFSET_STEP / 2)
229
- expect(offset1_neg).toBe(OFFSET_STEP / 2)
230
- })
231
-
232
- it('correctly calculates absolute coordinates for deeply nested nodes', () => {
233
- const nodes: Node<FlowNodeData>[] = [
234
- groupNode('g1', 'G1', {x: 100, y: 100}),
235
- {...groupNode('g2', 'G2', {x: 50, y: 50}), parentId: 'g1'} as Node<FlowNodeData>,
236
- {...serviceNode('n1', 'N1', undefined, {x: 10, y: 10}), parentId: 'g2'} as Node<FlowNodeData>
237
- ]
238
- // n1 abs position should be: g1(100,100) + g2(50,50) + n1(10,10) = (160, 160)
239
-
240
- // We'll create another node n2 and an edge n1 -> n2.
241
- const nodesFull = [
242
- ...nodes,
243
- serviceNode('n2', 'N2', undefined, {x: 500, y: 500})
244
- ]
245
- const edges = [
246
- {id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
247
- {id: 'e2', source: 'n3', target: 'n4', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
248
- ]
249
-
250
- // If n1 is at (160, 160) and n2 is at (500, 500)
251
- const n3 = serviceNode('n3', 'N3', undefined, {x: 160, y: 170})
252
- const n4 = serviceNode('n4', 'N4', undefined, {x: 500, y: 510})
253
- const nodesExtra = [...nodesFull, n3, n4]
254
-
255
- // e1 should be compared with other edges if they share handle or nodes
256
- // Let's use same handle for easier testing of ordering
257
- const edgesShared = [
258
- {id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
259
- {id: 'e2', source: 'n3', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
260
- ]
261
-
262
- // n1 abs y=160. n3 abs y=170.
263
- // e1 should get -4, e2 should get 4.
264
-
265
- const o1 = calculateEdgeOffset('e1', nodesExtra, edgesShared, false)
266
- const o2 = calculateEdgeOffset('e2', nodesExtra, edgesShared, false)
267
-
268
- expect(o1).toBe(-OFFSET_STEP / 2)
269
- expect(o2).toBe(OFFSET_STEP / 2)
270
- })
271
- })
1
+ import {describe, it, expect} from 'vitest'
2
+ import type {Edge, Node} from '@xyflow/svelte'
3
+ import {layoutFlowGraph, calculateEdgeOffset, OFFSET_STEP} from './layout'
4
+ import type {FlowEdgeData, FlowGraphInput, FlowNodeData} from '$shared/flow-types'
5
+
6
+ // Basic helpers to build nodes/edges with minimal required data
7
+ function serviceNode(id: string, label = id, serviceType?: string, position = {x: 0, y: 0}): Node<FlowNodeData> {
8
+ return {
9
+ id,
10
+ data: {label, kind: 'service', serviceType},
11
+ position,
12
+ type: 'service'
13
+ } as unknown as Node<FlowNodeData>
14
+ }
15
+
16
+ function groupNode(id: string, label = id, position = {x: 0, y: 0}): Node<FlowNodeData> {
17
+ return {
18
+ id,
19
+ data: {label, kind: 'group'},
20
+ position,
21
+ type: 'serviceGroup'
22
+ } as unknown as Node<FlowNodeData>
23
+ }
24
+
25
+ function edge(id: string, source: string, target: string, data: FlowEdgeData = {}): Edge<FlowEdgeData> {
26
+ return {
27
+ id,
28
+ source,
29
+ target,
30
+ data
31
+ } as Edge<FlowEdgeData>
32
+ }
33
+
34
+ // Silence verbose debug logs during tests
35
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
36
+ const noop = () => {
37
+ }
38
+ // @ts-expect-error allow override in test env
39
+ console.debug = noop
40
+ // @ts-expect-error allow override in test env
41
+ console.log = noop
42
+
43
+ describe('layoutFlowGraph', () => {
44
+ it('returns positions for nodes and preserves signature', async () => {
45
+ const input: FlowGraphInput = {
46
+ groupNodes: [],
47
+ serviceNodes: [serviceNode('s1', 'Service 1')],
48
+ edges: [],
49
+ signature: 'sig-1'
50
+ }
51
+
52
+ const out = await layoutFlowGraph(input)
53
+
54
+ expect(out.signature).toBe(input.signature)
55
+ expect(out.nodes.length).toBeGreaterThanOrEqual(1)
56
+
57
+ const n = out.nodes.find((x) => x.id === 's1')
58
+ expect(n).toBeTruthy()
59
+ expect(n?.position?.x).toBeDefined()
60
+ expect(n?.position?.y).toBeDefined()
61
+ })
62
+
63
+ it('keeps nodes and edges ids and data', async () => {
64
+ const input: FlowGraphInput = {
65
+ groupNodes: [],
66
+ serviceNodes: [serviceNode('a', 'Service A', 'lambda'), serviceNode('b', 'Service B', 'sqs')],
67
+ edges: [edge('e1', 'a', 'b', {label: 'A->B', direction: 'outgoing'})],
68
+ signature: 'sig-2'
69
+ }
70
+
71
+ const out = await layoutFlowGraph(input)
72
+
73
+ // Nodes should be present
74
+ const nodeA = out.nodes.find((n) => n.id === 'a')
75
+ const nodeB = out.nodes.find((n) => n.id === 'b')
76
+ expect(nodeA).toBeTruthy()
77
+ expect(nodeB).toBeTruthy()
78
+
79
+ // Verify component-specific data is preserved
80
+ expect(nodeA?.data.label).toBe('Service A')
81
+ expect(nodeA?.data.serviceType).toBe('lambda')
82
+ expect(nodeB?.data.serviceType).toBe('sqs')
83
+
84
+ // Edge should be preserved with same ids and data
85
+ expect(out.edges.length).toBe(1)
86
+ const e = out.edges[0]
87
+ expect(e.id).toBe('e1')
88
+ expect(e.source).toBe('a')
89
+ expect(e.target).toBe('b')
90
+ expect(e.data?.label).toBe('A->B')
91
+ expect(e.data?.direction).toBe('outgoing')
92
+ })
93
+
94
+ it('handles a simple group with a child service node', async () => {
95
+ const g = groupNode('g1', 'Group 1')
96
+ const s = {...serviceNode('s1', 'Service in group'), parentId: 'g1'} as unknown as Node<FlowNodeData>
97
+
98
+ const input: FlowGraphInput = {
99
+ groupNodes: [g],
100
+ serviceNodes: [s],
101
+ edges: [],
102
+ signature: 'sig-3'
103
+ }
104
+
105
+ const out = await layoutFlowGraph(input)
106
+
107
+ // Expect both the group and the service node to be present in flattened output
108
+ const groupOut = out.nodes.find((n) => n.id === 'g1')
109
+ const childOut = out.nodes.find((n) => n.id === 's1')
110
+
111
+ expect(groupOut).toBeTruthy()
112
+ expect(childOut).toBeTruthy()
113
+
114
+ // Both should have positions
115
+ expect(groupOut?.position?.x).toBeDefined()
116
+ expect(groupOut?.position?.y).toBeDefined()
117
+ expect(childOut?.position?.x).toBeDefined()
118
+ expect(childOut?.position?.y).toBeDefined()
119
+ })
120
+ })
121
+
122
+ describe('calculateEdgeOffset', () => {
123
+ it('returns 0 for a single edge', () => {
124
+ const nodes: Node<FlowNodeData>[] = [serviceNode('s1'), serviceNode('t1')]
125
+ const edges: Edge<FlowEdgeData>[] = [
126
+ {id: 'e1', source: 's1', target: 't1', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
127
+ ]
128
+
129
+ expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(0)
130
+ expect(calculateEdgeOffset('e1', nodes, edges, false)).toBe(0)
131
+ })
132
+
133
+ it('spreads multiple edges and sorts them by target position', () => {
134
+ const nodes: Node<FlowNodeData>[] = [
135
+ serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
136
+ serviceNode('t1', 'Top Target', undefined, {x: 200, y: 0}),
137
+ serviceNode('t2', 'Bottom Target', undefined, {x: 200, y: 200})
138
+ ]
139
+ const edges: Edge<FlowEdgeData>[] = [
140
+ {id: 'e_to_t1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
141
+ {id: 'e_to_t2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>
142
+ ]
143
+
144
+ // e_to_t1 connects to t1 (y=0)
145
+ // e_to_t2 connects to t2 (y=200)
146
+ // Sorted order: e_to_t1, e_to_t2
147
+ // index for e_to_t1 = 0. index for e_to_t2 = 1.
148
+ // offsets = (index - 0.5) * OFFSET_STEP
149
+
150
+ expect(calculateEdgeOffset('e_to_t1', nodes, edges, true)).toBe(-OFFSET_STEP / 2)
151
+ expect(calculateEdgeOffset('e_to_t2', nodes, edges, true)).toBe(OFFSET_STEP / 2)
152
+ })
153
+
154
+ it('spreads three edges correctly', () => {
155
+ const nodes: Node<FlowNodeData>[] = [
156
+ serviceNode('s1', 'Source', undefined, {x: 0, y: 100}),
157
+ serviceNode('t1', 'T1', undefined, {x: 200, y: 0}),
158
+ serviceNode('t2', 'T2', undefined, {x: 200, y: 100}),
159
+ serviceNode('t3', 'T3', undefined, {x: 200, y: 200})
160
+ ]
161
+ const edges: Edge<FlowEdgeData>[] = [
162
+ {id: 'e1', source: 's1', target: 't1', sourceHandle: 'output'} as Edge<FlowEdgeData>,
163
+ {id: 'e2', source: 's1', target: 't2', sourceHandle: 'output'} as Edge<FlowEdgeData>,
164
+ {id: 'e3', source: 's1', target: 't3', sourceHandle: 'output'} as Edge<FlowEdgeData>
165
+ ]
166
+
167
+ // Sorted order: e1 (y=0), e2 (y=100), e3 (y=200)
168
+ // index: 0, 1, 2. (n-1)/2 = 1.
169
+ // offsets: (0-1)*OFFSET_STEP, (1-1)*OFFSET_STEP, (2-1)*OFFSET_STEP
170
+
171
+ expect(calculateEdgeOffset('e1', nodes, edges, true)).toBe(-OFFSET_STEP)
172
+ expect(calculateEdgeOffset('e2', nodes, edges, true)).toBe(0)
173
+ expect(calculateEdgeOffset('e3', nodes, edges, true)).toBe(OFFSET_STEP)
174
+ })
175
+
176
+ it('correctly calculates offsets for nodes in groups vs orphans using absolute coordinates', () => {
177
+ // Node 1 (orphan) at x=10, y=60
178
+ // Group 1 at x=422, y=67
179
+ // Node 2 (child of Group 1) at x=10, y=160 (relative) -> Abs: y = 67 + 160 = 227
180
+ // Node 3 (child of Group 1) at x=350, y=260 (relative) -> Abs: y = 67 + 260 = 327
181
+ // Edges: Node 1 -> Node 3 (e1), Node 2 -> Node 3 (e2)
182
+ // Both connect to Node 3's input handle (target).
183
+ const nodes: Node<FlowNodeData>[] = [
184
+ serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
185
+ groupNode('g1', 'Group 1', {x: 422, y: 67}),
186
+ {...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
187
+ {...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
188
+ ]
189
+ const edges: Edge<FlowEdgeData>[] = [
190
+ {id: 'e1', source: 'n1', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>,
191
+ {id: 'e2', source: 'n2', target: 'n3', targetHandle: 'input'} as Edge<FlowEdgeData>
192
+ ]
193
+
194
+ // Absolute positions:
195
+ // n1 (abs y=60)
196
+ // n2 (abs y=67+160=227)
197
+ // Both are sources of Node 3.
198
+ // Order should be e1 (60), e2 (227).
199
+ // Offset for e1: -4, Offset for e2: 4.
200
+ // This confirms n1's edge is "above" n2's edge (smaller Y offset).
201
+ // The user said: "verify if the connection coming from the second node is above of the connection coming from the first node."
202
+ // Maybe the user meant if it WAS above? No, I'll just check if e1 is above e2.
203
+
204
+ const offset1 = calculateEdgeOffset('e1', nodes, edges, false) // target offset for e1
205
+ const offset2 = calculateEdgeOffset('e2', nodes, edges, false) // target offset for e2
206
+
207
+ // Expected: offset1 < offset2 because n1 (abs 60) < n2 (abs 227)
208
+ expect(offset1).toBe(-OFFSET_STEP / 2)
209
+ expect(offset2).toBe(OFFSET_STEP / 2)
210
+
211
+ // Now if the parent was at y=-200
212
+ // n2 (abs y=-200+160=-40)
213
+ // n1 (abs y=60)
214
+ // Now n2 is HIGHER than n1.
215
+ // Order should be e2 (-40), e1 (60).
216
+ // Offset for e2: -4, Offset for e1: 4.
217
+ const nodes2: Node<FlowNodeData>[] = [
218
+ serviceNode('n1', 'Orphan', undefined, {x: 10, y: 60}),
219
+ groupNode('g1', 'Group 1', {x: 422, y: -200}),
220
+ {...serviceNode('n2', 'Child 2', undefined, {x: 10, y: 160}), parentId: 'g1'} as Node<FlowNodeData>,
221
+ {...serviceNode('n3', 'Target 3', undefined, {x: 350, y: 260}), parentId: 'g1'} as Node<FlowNodeData>
222
+ ]
223
+
224
+ const offset1_neg = calculateEdgeOffset('e1', nodes2, edges, false)
225
+ const offset2_neg = calculateEdgeOffset('e2', nodes2, edges, false)
226
+
227
+ // Expected: offset2_neg < offset1_neg because n2 (abs -40) < n1 (abs 60)
228
+ expect(offset2_neg).toBe(-OFFSET_STEP / 2)
229
+ expect(offset1_neg).toBe(OFFSET_STEP / 2)
230
+ })
231
+
232
+ it('correctly calculates absolute coordinates for deeply nested nodes', () => {
233
+ const nodes: Node<FlowNodeData>[] = [
234
+ groupNode('g1', 'G1', {x: 100, y: 100}),
235
+ {...groupNode('g2', 'G2', {x: 50, y: 50}), parentId: 'g1'} as Node<FlowNodeData>,
236
+ {...serviceNode('n1', 'N1', undefined, {x: 10, y: 10}), parentId: 'g2'} as Node<FlowNodeData>
237
+ ]
238
+ // n1 abs position should be: g1(100,100) + g2(50,50) + n1(10,10) = (160, 160)
239
+
240
+ // We'll create another node n2 and an edge n1 -> n2.
241
+ const nodesFull = [
242
+ ...nodes,
243
+ serviceNode('n2', 'N2', undefined, {x: 500, y: 500})
244
+ ]
245
+ const edges = [
246
+ {id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
247
+ {id: 'e2', source: 'n3', target: 'n4', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
248
+ ]
249
+
250
+ // If n1 is at (160, 160) and n2 is at (500, 500)
251
+ const n3 = serviceNode('n3', 'N3', undefined, {x: 160, y: 170})
252
+ const n4 = serviceNode('n4', 'N4', undefined, {x: 500, y: 510})
253
+ const nodesExtra = [...nodesFull, n3, n4]
254
+
255
+ // e1 should be compared with other edges if they share handle or nodes
256
+ // Let's use same handle for easier testing of ordering
257
+ const edgesShared = [
258
+ {id: 'e1', source: 'n1', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>,
259
+ {id: 'e2', source: 'n3', target: 'n2', sourceHandle: 'output', targetHandle: 'input'} as Edge<FlowEdgeData>
260
+ ]
261
+
262
+ // n1 abs y=160. n3 abs y=170.
263
+ // e1 should get -4, e2 should get 4.
264
+
265
+ const o1 = calculateEdgeOffset('e1', nodesExtra, edgesShared, false)
266
+ const o2 = calculateEdgeOffset('e2', nodesExtra, edgesShared, false)
267
+
268
+ expect(o1).toBe(-OFFSET_STEP / 2)
269
+ expect(o2).toBe(OFFSET_STEP / 2)
270
+ })
271
+ })