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

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 (250) hide show
  1. package/.aws-icons-last-updated +1 -0
  2. package/.svelte-kit/ambient.d.ts +263 -0
  3. package/.svelte-kit/generated/client/app.js +31 -0
  4. package/.svelte-kit/generated/client/matchers.js +1 -0
  5. package/.svelte-kit/generated/client/nodes/0.js +1 -0
  6. package/.svelte-kit/generated/client/nodes/1.js +1 -0
  7. package/.svelte-kit/generated/client/nodes/2.js +3 -0
  8. package/.svelte-kit/generated/client/nodes/3.js +3 -0
  9. package/.svelte-kit/generated/client-optimized/app.js +31 -0
  10. package/.svelte-kit/generated/client-optimized/matchers.js +1 -0
  11. package/.svelte-kit/generated/client-optimized/nodes/0.js +1 -0
  12. package/.svelte-kit/generated/client-optimized/nodes/1.js +1 -0
  13. package/.svelte-kit/generated/client-optimized/nodes/2.js +3 -0
  14. package/.svelte-kit/generated/client-optimized/nodes/3.js +3 -0
  15. package/.svelte-kit/generated/root.js +3 -0
  16. package/.svelte-kit/generated/root.svelte +68 -0
  17. package/.svelte-kit/generated/server/internal.js +53 -0
  18. package/.svelte-kit/non-ambient.d.ts +43 -0
  19. package/.svelte-kit/output/client/.vite/manifest.json +175 -0
  20. package/.svelte-kit/output/client/_app/immutable/assets/0.Czt_67iE.css +1 -0
  21. package/.svelte-kit/output/client/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +1 -0
  22. package/.svelte-kit/output/client/_app/immutable/assets/helpers.ysDrpaDf.css +1 -0
  23. package/.svelte-kit/output/client/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
  24. package/.svelte-kit/output/client/_app/immutable/chunks/BlLuv0eP.js +46 -0
  25. package/.svelte-kit/output/client/_app/immutable/chunks/CSBHmwYv.js +1 -0
  26. package/.svelte-kit/output/client/_app/immutable/chunks/CTCi5ueQ.js +1 -0
  27. package/.svelte-kit/output/client/_app/immutable/chunks/CfOzjaik.js +2 -0
  28. package/.svelte-kit/output/client/_app/immutable/chunks/D4PdvFNs.js +1 -0
  29. package/.svelte-kit/output/client/_app/immutable/chunks/DXgP-QUS.js +2 -0
  30. package/.svelte-kit/output/client/_app/immutable/chunks/DlbDC5An.js +1 -0
  31. package/.svelte-kit/output/client/_app/immutable/chunks/wRWe7aK9.js +1 -0
  32. package/.svelte-kit/output/client/_app/immutable/entry/app.ConrMuHl.js +2 -0
  33. package/.svelte-kit/output/client/_app/immutable/entry/start.Bm6FyGme.js +1 -0
  34. package/.svelte-kit/output/client/_app/immutable/nodes/0.d3cL-ETU.js +1 -0
  35. package/.svelte-kit/output/client/_app/immutable/nodes/1.D6z9rPGv.js +1 -0
  36. package/.svelte-kit/output/client/_app/immutable/nodes/2.CLD-8chl.js +1 -0
  37. package/.svelte-kit/output/client/_app/immutable/nodes/3.DXYeBoel.js +1 -0
  38. package/.svelte-kit/output/client/_app/version.json +1 -0
  39. package/.svelte-kit/output/client/libavoid.wasm +0 -0
  40. package/.svelte-kit/output/client/static/robots.txt +3 -0
  41. package/.svelte-kit/output/prerendered/dependencies/_app/env.js +1 -0
  42. package/.svelte-kit/output/server/.vite/manifest.json +224 -0
  43. package/.svelte-kit/output/server/_app/immutable/assets/LoadingOverlay.DBbe6V8W.css +1 -0
  44. package/.svelte-kit/output/server/_app/immutable/assets/_layout.Czt_67iE.css +1 -0
  45. package/.svelte-kit/output/server/_app/immutable/assets/_page.D9P41uDZ.css +1 -0
  46. package/.svelte-kit/output/server/chunks/ErrorDisplay.js +59 -0
  47. package/.svelte-kit/output/server/chunks/LoadingOverlay.js +12 -0
  48. package/.svelte-kit/output/server/chunks/LoadingOverlay.svelte_svelte_type_style_lang.js +1671 -0
  49. package/.svelte-kit/output/server/chunks/connections.js +33 -0
  50. package/.svelte-kit/output/server/chunks/diagram.js +7 -0
  51. package/.svelte-kit/output/server/chunks/environment.js +34 -0
  52. package/.svelte-kit/output/server/chunks/equality.js +57 -0
  53. package/.svelte-kit/output/server/chunks/exports.js +174 -0
  54. package/.svelte-kit/output/server/chunks/false.js +4 -0
  55. package/.svelte-kit/output/server/chunks/index.js +59 -0
  56. package/.svelte-kit/output/server/chunks/index2.js +2939 -0
  57. package/.svelte-kit/output/server/chunks/index3.js +20 -0
  58. package/.svelte-kit/output/server/chunks/internal.js +1017 -0
  59. package/.svelte-kit/output/server/chunks/shared.js +770 -0
  60. package/.svelte-kit/output/server/chunks/utils.js +43 -0
  61. package/.svelte-kit/output/server/entries/pages/_error.svelte.js +64 -0
  62. package/.svelte-kit/output/server/entries/pages/_layout.svelte.js +65 -0
  63. package/.svelte-kit/output/server/entries/pages/_page.svelte.js +3991 -0
  64. package/.svelte-kit/output/server/entries/pages/_page.ts.js +30 -0
  65. package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.svelte.js +67 -0
  66. package/.svelte-kit/output/server/entries/pages/group/_groupId_/_page.ts.js +47 -0
  67. package/.svelte-kit/output/server/index.js +3747 -0
  68. package/.svelte-kit/output/server/internal.js +13 -0
  69. package/.svelte-kit/output/server/manifest-full.js +47 -0
  70. package/.svelte-kit/output/server/manifest.js +47 -0
  71. package/.svelte-kit/output/server/nodes/0.js +8 -0
  72. package/.svelte-kit/output/server/nodes/1.js +8 -0
  73. package/.svelte-kit/output/server/nodes/2.js +10 -0
  74. package/.svelte-kit/output/server/nodes/3.js +10 -0
  75. package/.svelte-kit/output/server/remote-entry.js +557 -0
  76. package/.svelte-kit/tsconfig.json +61 -0
  77. package/.svelte-kit/types/route_meta_data.json +8 -0
  78. package/.svelte-kit/types/src/routes/$types.d.ts +26 -0
  79. package/.svelte-kit/types/src/routes/group/[groupId]/$types.d.ts +21 -0
  80. package/.svelte-kit/types/src/routes/group/[groupId]/proxy+page.ts +49 -0
  81. package/.svelte-kit/types/src/routes/proxy+page.ts +33 -0
  82. package/build/_app/env.js +1 -0
  83. package/build/_app/env.js.br +1 -0
  84. package/build/_app/env.js.gz +0 -0
  85. package/build/_app/immutable/assets/0.Czt_67iE.css +1 -0
  86. package/build/_app/immutable/assets/0.Czt_67iE.css.br +0 -0
  87. package/build/_app/immutable/assets/0.Czt_67iE.css.gz +0 -0
  88. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css +1 -0
  89. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.br +0 -0
  90. package/build/_app/immutable/assets/TeamContactCard.Dxj5nUCr.css.gz +0 -0
  91. package/build/_app/immutable/assets/helpers.ysDrpaDf.css +1 -0
  92. package/build/_app/immutable/assets/helpers.ysDrpaDf.css.br +0 -0
  93. package/build/_app/immutable/assets/helpers.ysDrpaDf.css.gz +0 -0
  94. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm +0 -0
  95. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.br +0 -0
  96. package/build/_app/immutable/assets/libavoid.DQJapW5w.wasm.gz +0 -0
  97. package/build/_app/immutable/chunks/BlLuv0eP.js +46 -0
  98. package/build/_app/immutable/chunks/BlLuv0eP.js.br +0 -0
  99. package/build/_app/immutable/chunks/BlLuv0eP.js.gz +0 -0
  100. package/build/_app/immutable/chunks/CSBHmwYv.js +1 -0
  101. package/build/_app/immutable/chunks/CSBHmwYv.js.br +0 -0
  102. package/build/_app/immutable/chunks/CSBHmwYv.js.gz +0 -0
  103. package/build/_app/immutable/chunks/CTCi5ueQ.js +1 -0
  104. package/build/_app/immutable/chunks/CTCi5ueQ.js.br +0 -0
  105. package/build/_app/immutable/chunks/CTCi5ueQ.js.gz +0 -0
  106. package/build/_app/immutable/chunks/CfOzjaik.js +2 -0
  107. package/build/_app/immutable/chunks/CfOzjaik.js.br +0 -0
  108. package/build/_app/immutable/chunks/CfOzjaik.js.gz +0 -0
  109. package/build/_app/immutable/chunks/D4PdvFNs.js +1 -0
  110. package/build/_app/immutable/chunks/D4PdvFNs.js.br +0 -0
  111. package/build/_app/immutable/chunks/D4PdvFNs.js.gz +0 -0
  112. package/build/_app/immutable/chunks/DXgP-QUS.js +2 -0
  113. package/build/_app/immutable/chunks/DXgP-QUS.js.br +0 -0
  114. package/build/_app/immutable/chunks/DXgP-QUS.js.gz +0 -0
  115. package/build/_app/immutable/chunks/DlbDC5An.js +1 -0
  116. package/build/_app/immutable/chunks/DlbDC5An.js.br +0 -0
  117. package/build/_app/immutable/chunks/DlbDC5An.js.gz +0 -0
  118. package/build/_app/immutable/chunks/wRWe7aK9.js +1 -0
  119. package/build/_app/immutable/chunks/wRWe7aK9.js.br +0 -0
  120. package/build/_app/immutable/chunks/wRWe7aK9.js.gz +0 -0
  121. package/build/_app/immutable/entry/app.ConrMuHl.js +2 -0
  122. package/build/_app/immutable/entry/app.ConrMuHl.js.br +0 -0
  123. package/build/_app/immutable/entry/app.ConrMuHl.js.gz +0 -0
  124. package/build/_app/immutable/entry/start.Bm6FyGme.js +1 -0
  125. package/build/_app/immutable/entry/start.Bm6FyGme.js.br +2 -0
  126. package/build/_app/immutable/entry/start.Bm6FyGme.js.gz +0 -0
  127. package/build/_app/immutable/nodes/0.d3cL-ETU.js +1 -0
  128. package/build/_app/immutable/nodes/0.d3cL-ETU.js.br +0 -0
  129. package/build/_app/immutable/nodes/0.d3cL-ETU.js.gz +0 -0
  130. package/build/_app/immutable/nodes/1.D6z9rPGv.js +1 -0
  131. package/build/_app/immutable/nodes/1.D6z9rPGv.js.br +0 -0
  132. package/build/_app/immutable/nodes/1.D6z9rPGv.js.gz +0 -0
  133. package/build/_app/immutable/nodes/2.CLD-8chl.js +1 -0
  134. package/build/_app/immutable/nodes/2.CLD-8chl.js.br +0 -0
  135. package/build/_app/immutable/nodes/2.CLD-8chl.js.gz +0 -0
  136. package/build/_app/immutable/nodes/3.DXYeBoel.js +1 -0
  137. package/build/_app/immutable/nodes/3.DXYeBoel.js.br +0 -0
  138. package/build/_app/immutable/nodes/3.DXYeBoel.js.gz +0 -0
  139. package/build/_app/version.json +1 -0
  140. package/build/_app/version.json.br +0 -0
  141. package/build/_app/version.json.gz +0 -0
  142. package/build/index.html +34 -0
  143. package/build/index.html.br +0 -0
  144. package/build/index.html.gz +0 -0
  145. package/build/libavoid.wasm +0 -0
  146. package/build/libavoid.wasm.br +0 -0
  147. package/build/libavoid.wasm.gz +0 -0
  148. package/build/static/robots.txt +3 -0
  149. package/coverage/coverage-final.json +6 -0
  150. package/coverage/coverage-summary.json +7 -0
  151. package/coverage/junit.xml +57 -0
  152. package/coverage/lcov-report/base.css +224 -0
  153. package/coverage/lcov-report/block-navigation.js +87 -0
  154. package/coverage/lcov-report/favicon.png +0 -0
  155. package/coverage/lcov-report/index.html +131 -0
  156. package/coverage/lcov-report/prettify.css +1 -0
  157. package/coverage/lcov-report/prettify.js +2 -0
  158. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  159. package/coverage/lcov-report/sorter.js +210 -0
  160. package/coverage/lcov-report/stores/index.html +116 -0
  161. package/coverage/lcov-report/stores/routingStore.ts.html +781 -0
  162. package/coverage/lcov-report/utils/flow/helpers.ts.html +127 -0
  163. package/coverage/lcov-report/utils/flow/index.html +161 -0
  164. package/coverage/lcov-report/utils/flow/layout.ts.html +805 -0
  165. package/coverage/lcov-report/utils/flow/serviceIds.ts.html +97 -0
  166. package/coverage/lcov-report/utils/flow/servicesGraph.ts.html +859 -0
  167. package/coverage/lcov.info +646 -0
  168. package/coverage/test-results.json +1 -0
  169. package/data/services/api/api-gateway.yaml +18 -0
  170. package/data/services/api/group-info.yaml +7 -0
  171. package/data/services/api/lambda-orders.yaml +21 -0
  172. package/data/services/api/lambda-products.yaml +15 -0
  173. package/data/services/api/lambda-users.yaml +15 -0
  174. package/data/services/compute/alb.yaml +15 -0
  175. package/data/services/compute/ecs-inventory.yaml +16 -0
  176. package/data/services/compute/ecs-notification.yaml +15 -0
  177. package/data/services/compute/group-info.yaml +7 -0
  178. package/data/services/data/dynamodb-notifications.yaml +12 -0
  179. package/data/services/data/dynamodb-orders.yaml +9 -0
  180. package/data/services/data/dynamodb-products.yaml +9 -0
  181. package/data/services/data/dynamodb-users.yaml +9 -0
  182. package/data/services/data/group-info.yaml +7 -0
  183. package/data/services/data/rds-postgres.yaml +9 -0
  184. package/data/services/data/redis.yaml +10 -0
  185. package/data/services/frontend/cloudfront.yaml +12 -0
  186. package/data/services/frontend/group-info.yaml +7 -0
  187. package/data/services/frontend/route53.yaml +15 -0
  188. package/data/services/frontend/s3-website.yaml +9 -0
  189. package/data/teams/cloud-shepherds.yaml +15 -0
  190. package/data/teams/data-wizards.yaml +15 -0
  191. package/data/teams/interface-architects.yaml +19 -0
  192. package/e2e/demo.test.ts +54 -0
  193. package/e2e/header-toolbar.simple.spec.ts +0 -0
  194. package/e2e/header-toolbar.spec.ts +53 -0
  195. package/e2e/layout.spec.ts +30 -0
  196. package/package.json +69 -0
  197. package/playwright.config.ts +10 -0
  198. package/plugins/mapper-data-plugin.ts +32 -0
  199. package/project.json +23 -0
  200. package/src/app.css +125 -0
  201. package/src/app.d.ts +31 -0
  202. package/src/app.html +11 -0
  203. package/src/lib/assets/favicon.svg +19 -0
  204. package/src/lib/components/EmptyState.svelte +37 -0
  205. package/src/lib/components/ErrorDisplay.svelte +82 -0
  206. package/src/lib/components/FlowCanvas.svelte +223 -0
  207. package/src/lib/components/GenericSidebarCard.svelte +44 -0
  208. package/src/lib/components/GroupDetailSidebar.svelte +31 -0
  209. package/src/lib/components/Header.svelte +57 -0
  210. package/src/lib/components/Legend.svelte +25 -0
  211. package/src/lib/components/LoadingOverlay.svelte +42 -0
  212. package/src/lib/components/LoadingSpinner.svelte +10 -0
  213. package/src/lib/components/ServiceDetailSidebar.svelte +90 -0
  214. package/src/lib/components/TeamContactCard.svelte +166 -0
  215. package/src/lib/components/flow/ExternalNode.svelte +45 -0
  216. package/src/lib/components/flow/MainGroupNode.svelte +24 -0
  217. package/src/lib/components/flow/ServiceGroupNode.svelte +17 -0
  218. package/src/lib/components/flow/ServiceNode.svelte +40 -0
  219. package/src/lib/components/flow/SnakeEdge.svelte +206 -0
  220. package/src/lib/components/flow/index.ts +6 -0
  221. package/src/lib/components/index.ts +12 -0
  222. package/src/lib/data/connections.ts +26 -0
  223. package/src/lib/data/groups.ts +11 -0
  224. package/src/lib/data/services.ts +12 -0
  225. package/src/lib/data/teams.ts +11 -0
  226. package/src/lib/index.ts +1 -0
  227. package/src/lib/state/theme.svelte.ts +21 -0
  228. package/src/lib/stores/diagram.ts +6 -0
  229. package/src/lib/stores/routingStore.test.ts +133 -0
  230. package/src/lib/stores/routingStore.ts +232 -0
  231. package/src/lib/utils/awsIcons.ts +117 -0
  232. package/src/lib/utils/flow/groupOverviewGraph.ts +73 -0
  233. package/src/lib/utils/flow/helpers.ts +14 -0
  234. package/src/lib/utils/flow/layout.test.ts +271 -0
  235. package/src/lib/utils/flow/layout.ts +240 -0
  236. package/src/lib/utils/flow/serviceIds.ts +5 -0
  237. package/src/lib/utils/flow/servicesGraph.test.ts +119 -0
  238. package/src/lib/utils/flow/servicesGraph.ts +258 -0
  239. package/src/routes/+error.svelte +36 -0
  240. package/src/routes/+layout.svelte +27 -0
  241. package/src/routes/+page.svelte +81 -0
  242. package/src/routes/+page.ts +31 -0
  243. package/src/routes/group/[groupId]/+page.svelte +102 -0
  244. package/src/routes/group/[groupId]/+page.ts +48 -0
  245. package/src/routes/layout.css +0 -0
  246. package/static/static/robots.txt +3 -0
  247. package/svelte.config.js +28 -0
  248. package/tailwind.config.js +12 -0
  249. package/tsconfig.json +22 -0
  250. package/vite.config.ts +81 -0
@@ -0,0 +1 @@
1
+ // place files you want to import through the `$lib` alias in this folder.
@@ -0,0 +1,21 @@
1
+ export class ThemeState {
2
+ isDark = $state(false);
3
+
4
+ constructor() {
5
+ if (typeof window !== 'undefined') {
6
+ const stored = localStorage.getItem('preferredTheme');
7
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
8
+ this.isDark = stored ? stored === 'dark' : prefersDark;
9
+ }
10
+ }
11
+
12
+ toggle() {
13
+ this.isDark = !this.isDark;
14
+ if (typeof window !== 'undefined') {
15
+ localStorage.setItem('preferredTheme', this.isDark ? 'dark' : 'light');
16
+ }
17
+ }
18
+ }
19
+
20
+ export const theme = new ThemeState();
21
+
@@ -0,0 +1,6 @@
1
+ import { writable, type Writable } from 'svelte/store';
2
+ import type {GroupInfo} from "$shared/types";
3
+
4
+ export const selectedGroup: Writable<GroupInfo | null> = writable(null);
5
+ export const showLegend: Writable<boolean> = writable(true);
6
+ export const logDiagramAction = writable(0);
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { routingStore } from './routingStore';
3
+ import type { XYPosition } from '@xyflow/svelte';
4
+
5
+ // Mock setTimeout to control the 16ms debounce
6
+ vi.useFakeTimers();
7
+
8
+ describe('routingStore', () => {
9
+ beforeEach(() => {
10
+ // Reset the store/router if possible?
11
+ // Since it's a singleton, we might need a reset method or just clear the maps.
12
+ // For now let's just assume we can test it incrementally.
13
+ });
14
+
15
+ it('registers nodes and routes an edge between them', async () => {
16
+ const nodeA = {
17
+ nodeId: 'a',
18
+ absPos: { x: 0, y: 0 },
19
+ width: 100,
20
+ height: 50,
21
+ isGroup: false
22
+ };
23
+ const nodeB = {
24
+ nodeId: 'b',
25
+ absPos: { x: 200, y: 200 },
26
+ width: 100,
27
+ height: 50,
28
+ isGroup: false
29
+ };
30
+
31
+ await routingStore.registerNode(nodeA);
32
+ await routingStore.registerNode(nodeB);
33
+
34
+ await routingStore.registerEdge({
35
+ edgeId: 'e1',
36
+ sourceNodeId: 'a',
37
+ targetNodeId: 'b',
38
+ sourcePos: { x: 100, y: 25 },
39
+ sourceSide: 'right',
40
+ targetPos: { x: 200, y: 225 },
41
+ targetSide: 'left'
42
+ });
43
+
44
+ // Trigger debounce
45
+ vi.advanceTimersByTime(20);
46
+
47
+ // Check if edge e1 has points
48
+ let results: Record<string, XYPosition[]> = {};
49
+ const unsubscribe = routingStore.subscribe(val => {
50
+ results = val;
51
+ });
52
+
53
+ expect(results['e1']).toBeDefined();
54
+ // Since we use shoulders (checkpoints), libavoid might still return 3 points if it's very direct,
55
+ // but typically it's 4+ for orthogonal snaking.
56
+ expect(results['e1'].length).toBeGreaterThanOrEqual(3);
57
+
58
+ unsubscribe();
59
+ });
60
+
61
+ it('nudges parallel edges apart', async () => {
62
+ // Vertically stacked nodes
63
+ const nodes = [
64
+ { nodeId: 's', absPos: { x: 0, y: 100 }, width: 100, height: 50, isGroup: false },
65
+ { nodeId: 't1', absPos: { x: 300, y: 0 }, width: 100, height: 50, isGroup: false },
66
+ { nodeId: 't2', absPos: { x: 300, y: 200 }, width: 100, height: 50, isGroup: false }
67
+ ];
68
+
69
+ for (const n of nodes) await routingStore.registerNode(n);
70
+
71
+ // Two edges from same source to different targets
72
+ // Their paths might want to share the same "lane" if they were routed individually
73
+ await routingStore.registerEdge({
74
+ edgeId: 'e1', sourceNodeId: 's', targetNodeId: 't1',
75
+ sourcePos: { x: 100, y: 125 }, sourceSide: 'right',
76
+ targetPos: { x: 300, y: 25 }, targetSide: 'left'
77
+ });
78
+ await routingStore.registerEdge({
79
+ edgeId: 'e2', sourceNodeId: 's', targetNodeId: 't2',
80
+ sourcePos: { x: 100, y: 125 }, sourceSide: 'right',
81
+ targetPos: { x: 300, y: 225 }, targetSide: 'left'
82
+ });
83
+
84
+ vi.advanceTimersByTime(20);
85
+
86
+ let results: Record<string, XYPosition[]> = {};
87
+ routingStore.subscribe(val => results = val)();
88
+
89
+ const p1 = results['e1'];
90
+ const p2 = results['e2'];
91
+
92
+ expect(p1).toBeDefined();
93
+ expect(p2).toBeDefined();
94
+
95
+ // Check vertical segments (trunks)
96
+ const getTrunkX = (pts: XYPosition[]) => {
97
+ const v = pts.find((p, i) => i > 0 && p.x === pts[i-1].x && Math.abs(p.y - pts[i-1].y) > 10);
98
+ return v ? v.x : null;
99
+ };
100
+
101
+ const x1 = getTrunkX(p1);
102
+ const x2 = getTrunkX(p2);
103
+
104
+ if (x1 !== null && x2 !== null) {
105
+ expect(Math.abs(x1 - x2)).toBeGreaterThanOrEqual(10); // Nudging working
106
+ }
107
+ });
108
+
109
+ it('unregisters an edge and stops returning its points', async () => {
110
+ await routingStore.registerEdge({
111
+ edgeId: 'e_remove',
112
+ sourceNodeId: 'a',
113
+ targetNodeId: 'b',
114
+ sourcePos: { x: 100, y: 25 },
115
+ sourceSide: 'right',
116
+ targetPos: { x: 200, y: 225 },
117
+ targetSide: 'left'
118
+ });
119
+
120
+ vi.advanceTimersByTime(20);
121
+
122
+ let results: Record<string, XYPosition[]> = {};
123
+ routingStore.subscribe(val => results = val)();
124
+ expect(results['e_remove']).toBeDefined();
125
+
126
+ routingStore.unregisterEdge('e_remove');
127
+
128
+ // Unregistering deletes from state immediately but also processes transaction
129
+ results = {};
130
+ routingStore.subscribe(val => results = val)();
131
+ expect(results['e_remove']).toBeUndefined();
132
+ });
133
+ });
@@ -0,0 +1,232 @@
1
+ import {writable} from 'svelte/store';
2
+ import type {XYPosition} from '@xyflow/svelte';
3
+ import {type Avoid, AvoidLib, type ConnRef, type Router, type ShapeRef} from 'libavoid-js';
4
+ import {OFFSET_STEP} from "$lib/utils/flow/layout";
5
+
6
+ // --- Types ---
7
+
8
+ export interface EdgeRoutingUpdate {
9
+ edgeId: string;
10
+ sourceNodeId: string;
11
+ targetNodeId: string;
12
+ sourcePos: XYPosition;
13
+ sourceSide: string;
14
+ targetPos: XYPosition;
15
+ targetSide: string;
16
+ }
17
+
18
+ export interface NodeRoutingUpdate {
19
+ nodeId: string;
20
+ absPos: XYPosition;
21
+ width: number;
22
+ height: number;
23
+ isGroup: boolean;
24
+ }
25
+
26
+ // --- Library Loading ---
27
+
28
+ let avoidLibPromise: Promise<void> | null = null;
29
+ let avoidLibInstance: Avoid | null = null;
30
+
31
+ async function ensureAvoidLibLoaded() {
32
+ if (avoidLibPromise) return avoidLibPromise;
33
+ if (typeof window === 'undefined') {
34
+ //We're in test mode in node, load the library directly from the file system
35
+ avoidLibPromise = AvoidLib.load();
36
+ } else {
37
+ avoidLibPromise = AvoidLib.load("/libavoid.wasm");
38
+ }
39
+ return avoidLibPromise;
40
+ }
41
+
42
+ async function getAvoidLib() {
43
+ await ensureAvoidLibLoaded();
44
+ avoidLibInstance ??= AvoidLib.getInstance();
45
+ return avoidLibInstance;
46
+ }
47
+
48
+ // --- Helpers ---
49
+
50
+ const getDirFlag = (pos: string) => {
51
+ const p = pos.toLowerCase();
52
+ if (p === 'right') return 8;
53
+ if (p === 'left') return 4;
54
+ if (p === 'top') return 1;
55
+ if (p === 'bottom') return 2;
56
+ return 15;
57
+ };
58
+
59
+ const getShoulderPoint = (avoid: Avoid, x: number, y: number, pos: string, dist: number) => {
60
+ switch (pos.toLowerCase()) {
61
+ case 'right': return new avoid.Point(x + dist, y);
62
+ case 'left': return new avoid.Point(x - dist, y);
63
+ case 'top': return new avoid.Point(x, y - dist);
64
+ case 'bottom': return new avoid.Point(x, y + dist);
65
+ default: return new avoid.Point(x, y);
66
+ }
67
+ };
68
+
69
+ // --- Store Implementation ---
70
+
71
+ function createRoutingStore() {
72
+ const { subscribe, set } = writable<Record<string, XYPosition[]>>({});
73
+
74
+ let router: Router | null = null;
75
+ let avoid: Avoid | null = null;
76
+
77
+ const nodes = new Map<string, ShapeRef>();
78
+ const nodesMetadata = new Map<string, XYPosition>();
79
+ const edges = new Map<string, ConnRef>();
80
+
81
+ let pendingTransaction = false;
82
+
83
+ async function init() {
84
+ if (router) return;
85
+ avoid = await getAvoidLib();
86
+ router = new avoid.Router(avoid.RouterFlag.OrthogonalRouting.value);
87
+
88
+ router.setRoutingParameter(avoid.RoutingParameter.reverseDirectionPenalty.valueOf(), 1000);
89
+ router.setRoutingParameter(avoid.RoutingParameter.portDirectionPenalty.valueOf(), 500);
90
+ router.setRoutingParameter(avoid.RoutingParameter.idealNudgingDistance.valueOf(), OFFSET_STEP);
91
+ router.setRoutingParameter(avoid.RoutingParameter.segmentPenalty.valueOf(), 50);
92
+
93
+ // Options for nudging
94
+ router.setRoutingOption(avoid.RoutingOption.nudgeOrthogonalSegmentsConnectedToShapes.valueOf(), true);
95
+ router.setRoutingOption(avoid.RoutingOption.nudgeOrthogonalTouchingColinearSegments.valueOf(), true);
96
+ }
97
+
98
+ function processTransaction() {
99
+ if (!router || pendingTransaction) return;
100
+
101
+ pendingTransaction = true;
102
+ // Batch to next frame (16ms)
103
+ setTimeout(() => {
104
+ if (!router) {
105
+ pendingTransaction = false;
106
+ return;
107
+ }
108
+
109
+ router.processTransaction();
110
+
111
+ const results: Record<string, XYPosition[]> = {};
112
+ edges.forEach((conn, id) => {
113
+ const route = conn.displayRoute();
114
+ const pts: XYPosition[] = [];
115
+ for (let i = 0; i < route.size(); i++) {
116
+ const p = route.at(i);
117
+ pts.push({ x: p.x, y: p.y });
118
+ }
119
+ results[id] = pts;
120
+ });
121
+
122
+ set(results);
123
+ pendingTransaction = false;
124
+ }, 16);
125
+ }
126
+
127
+ return {
128
+ subscribe,
129
+
130
+ registerNode: async (data: NodeRoutingUpdate) => {
131
+ await init();
132
+ if (!router || !avoid) return;
133
+
134
+ if (data.isGroup) {
135
+ if (nodes.has(data.nodeId)) {
136
+ const shape = nodes.get(data.nodeId)!;
137
+ router.deleteShape(shape);
138
+ nodes.delete(data.nodeId);
139
+ nodesMetadata.delete(data.nodeId);
140
+ }
141
+ return;
142
+ }
143
+
144
+ nodesMetadata.set(data.nodeId, data.absPos);
145
+
146
+ const poly = new avoid.Polygon(4);
147
+ poly.setPoint(0, new avoid.Point(data.absPos.x, data.absPos.y));
148
+ poly.setPoint(1, new avoid.Point(data.absPos.x + data.width, data.absPos.y));
149
+ poly.setPoint(2, new avoid.Point(data.absPos.x + data.width, data.absPos.y + data.height));
150
+ poly.setPoint(3, new avoid.Point(data.absPos.x, data.absPos.y + data.height));
151
+
152
+ if (nodes.has(data.nodeId)) {
153
+ const shape = nodes.get(data.nodeId)!;
154
+ //@ts-ignore - libavoid typings are wrong, moveShape_poly actually needs three args
155
+ router.moveShape_poly(shape, poly, false);
156
+ } else {
157
+ const shape = new avoid.ShapeRef(router, poly);
158
+ nodes.set(data.nodeId, shape);
159
+ }
160
+
161
+ processTransaction();
162
+ },
163
+
164
+ registerEdge: async (data: EdgeRoutingUpdate) => {
165
+ await init();
166
+ if (!router || !avoid) return;
167
+
168
+ const sShape = nodes.get(data.sourceNodeId);
169
+ const tShape = nodes.get(data.targetNodeId);
170
+
171
+ if (!sShape || !tShape) return;
172
+
173
+ const createConnEnd = (nodeId: string, x: number, y: number, side: string, shape: ShapeRef) => {
174
+ if (!avoid) throw new Error('Avoid not loaded');
175
+ const pinClass = Array.from(data.edgeId).reduce((a, b) => a + (b.codePointAt(0) ?? 0), 0);
176
+
177
+ // Convert absolute coords to relative coords for the pin (libavoid expects relative to shape's top-left)
178
+ const abs = nodesMetadata.get(nodeId);
179
+ const xOff = x - (abs?.x ?? 0);
180
+ const yOff = y - (abs?.y ?? 0);
181
+
182
+ const pin = new avoid.ShapeConnectionPin(shape, pinClass, xOff, yOff, false, -40, getDirFlag(side));
183
+ pin.setExclusive(true);
184
+ return new avoid.ConnEnd(shape, pinClass);
185
+ };
186
+
187
+ const srcEnd = createConnEnd(data.sourceNodeId, data.sourcePos.x, data.sourcePos.y, data.sourceSide, sShape);
188
+ const dstEnd = createConnEnd(data.targetNodeId, data.targetPos.x, data.targetPos.y, data.targetSide, tShape);
189
+
190
+ let conn: ConnRef;
191
+ if (edges.has(data.edgeId)) {
192
+ conn = edges.get(data.edgeId)!;
193
+ conn.setSourceEndpoint(srcEnd);
194
+ conn.setDestEndpoint(dstEnd);
195
+ } else {
196
+ conn = new avoid.ConnRef(router, srcEnd, dstEnd);
197
+ edges.set(data.edgeId, conn);
198
+ }
199
+
200
+ // Checkpoints for shoulders
201
+ const checkpoints = new avoid.CheckpointVector();
202
+ checkpoints.push_back(new avoid.Checkpoint(getShoulderPoint(avoid, data.sourcePos.x, data.sourcePos.y, data.sourceSide, 10)));
203
+ checkpoints.push_back(new avoid.Checkpoint(getShoulderPoint(avoid, data.targetPos.x, data.targetPos.y, data.targetSide, 10)));
204
+ conn.setRoutingCheckpoints(checkpoints);
205
+
206
+ processTransaction();
207
+ },
208
+
209
+ unregisterEdge: (id: string) => {
210
+ if (!router || !edges.has(id)) return;
211
+ const conn = edges.get(id)!;
212
+ router.deleteConnector(conn);
213
+ edges.delete(id);
214
+
215
+ set(Object.fromEntries(
216
+ Array.from(edges.entries()).map(([id, conn]) => {
217
+ const route = conn.displayRoute();
218
+ const pts: XYPosition[] = [];
219
+ for (let i = 0; i < route.size(); i++) {
220
+ const p = route.at(i);
221
+ pts.push({ x: p.x, y: p.y });
222
+ }
223
+ return [id, pts];
224
+ })
225
+ ));
226
+
227
+ processTransaction();
228
+ }
229
+ };
230
+ }
231
+
232
+ export const routingStore = createRoutingStore();
@@ -0,0 +1,117 @@
1
+ import { ServiceType } from '@k-4u/resource-mapper-shared'
2
+
3
+ type IconLookup = Record<ServiceType, string>
4
+
5
+ //@ts-ignore We ignore a bunch of missing options here for now. This should probably be generated from the service definitions at some point, but for now we can just hardcode the most common ones.
6
+ const SERVICE_TYPE_TO_ICON: IconLookup = {
7
+ [ServiceType.EC2]: 'Compute/EC2.svg',
8
+ [ServiceType.ECS]: 'Containers/Elastic-Container-Service.svg',
9
+ [ServiceType.EKS]: 'Containers/Elastic-Kubernetes-Service.svg',
10
+ [ServiceType.LAMBDA]: 'Compute/Lambda.svg',
11
+ [ServiceType.FARGATE]: 'Compute/Fargate.svg',
12
+ [ServiceType.BATCH]: 'Compute/Batch.svg',
13
+ [ServiceType.ELASTIC_BEANSTALK]: 'Compute/Elastic-Beanstalk.svg',
14
+ [ServiceType.APP_RUNNER]: 'Containers/App-Runner.svg',
15
+ [ServiceType.ECR]: 'Containers/Elastic-Container-Registry.svg',
16
+ [ServiceType.ECS_ANYWHERE]: 'Containers/ECS-Anywhere.svg',
17
+ [ServiceType.EKS_ANYWHERE]: 'Containers/EKS-Anywhere.svg',
18
+ [ServiceType.S3]: 'Storage/Simple-Storage-Service.svg',
19
+ [ServiceType.EFS]: 'Storage/EFS.svg',
20
+ [ServiceType.EBS]: 'Storage/Elastic-Block-Store.svg',
21
+ [ServiceType.FSX]: 'Storage/FSx.svg',
22
+ [ServiceType.STORAGE_GATEWAY]: 'Storage/Storage-Gateway.svg',
23
+ [ServiceType.BACKUP]: 'Storage/Backup.svg',
24
+ [ServiceType.S3_GLACIER]: 'Storage/Simple-Storage-Service-Glacier.svg',
25
+ [ServiceType.RDS]: 'Database/RDS.svg',
26
+ [ServiceType.DYNAMODB]: 'Database/DynamoDB.svg',
27
+ [ServiceType.ELASTICACHE]: 'Database/ElastiCache.svg',
28
+ [ServiceType.VALKEY]: 'Database/ElastiCache.svg',
29
+ [ServiceType.AURORA]: 'Database/Aurora.svg',
30
+ [ServiceType.NEPTUNE]: 'Database/Neptune.svg',
31
+ [ServiceType.DOCUMENTDB]: 'Database/DocumentDB.svg',
32
+ [ServiceType.KEYSPACES]: 'Database/Keyspaces.svg',
33
+ [ServiceType.TIMESTREAM]: 'Database/Timestream.svg',
34
+ [ServiceType.MEMORYDB]: 'Database/MemoryDB-for-Redis.svg',
35
+ [ServiceType.QLDB]: 'Database/Quantum-Ledger-Database.svg',
36
+ [ServiceType.VPC]: 'Networking-Content-Delivery/Virtual-Private-Cloud.svg',
37
+ [ServiceType.CLOUDFRONT]: 'Networking-Content-Delivery/CloudFront.svg',
38
+ [ServiceType.ROUTE53]: 'Networking-Content-Delivery/Route-53.svg',
39
+ [ServiceType.API_GATEWAY]: 'App-Integration/API-Gateway.svg',
40
+ [ServiceType.DIRECT_CONNECT]: 'Networking-Content-Delivery/Direct-Connect.svg',
41
+ [ServiceType.APP_MESH]: 'Networking-Content-Delivery/App-Mesh.svg',
42
+ [ServiceType.CLOUD_MAP]: 'Networking-Content-Delivery/Cloud-Map.svg',
43
+ [ServiceType.GLOBAL_ACCELERATOR]: 'Networking-Content-Delivery/Global-Accelerator.svg',
44
+ [ServiceType.ALB]: 'Networking-Content-Delivery/Elastic-Load-Balancing.svg',
45
+ [ServiceType.NLB]: 'Networking-Content-Delivery/Elastic-Load-Balancing.svg',
46
+ [ServiceType.ELB]: 'Networking-Content-Delivery/Elastic-Load-Balancing.svg',
47
+ [ServiceType.ELASTIC_LOAD_BALANCING]: 'Networking-Content-Delivery/Elastic-Load-Balancing.svg',
48
+ [ServiceType.SQS]: 'App-Integration/Simple-Queue-Service.svg',
49
+ [ServiceType.SNS]: 'App-Integration/Simple-Notification-Service.svg',
50
+ [ServiceType.EVENTBRIDGE]: 'App-Integration/EventBridge.svg',
51
+ [ServiceType.EVENTBRIDGE_RULE]: 'App-Integration/EventBridge.svg',
52
+ [ServiceType.STEP_FUNCTIONS]: 'App-Integration/Step-Functions.svg',
53
+ [ServiceType.SWF]: 'App-Integration/Step-Functions.svg',
54
+ [ServiceType.MQ]: 'App-Integration/MQ.svg',
55
+ [ServiceType.APPSYNC]: 'Front-End-Web-Mobile/AppSync.svg',
56
+ [ServiceType.KINESIS]: 'Analytics/Kinesis.svg',
57
+ [ServiceType.KINESIS_FIREHOSE]: 'Analytics/Kinesis-Firehose.svg',
58
+ [ServiceType.KINESIS_ANALYTICS]: 'Analytics/Kinesis-Data-Analytics.svg',
59
+ [ServiceType.ATHENA]: 'Analytics/Athena.svg',
60
+ [ServiceType.EMR]: 'Analytics/EMR.svg',
61
+ [ServiceType.GLUE]: 'Analytics/Glue.svg',
62
+ [ServiceType.REDSHIFT]: 'Analytics/Redshift.svg',
63
+ [ServiceType.QUICKSIGHT]: 'Analytics/QuickSight.svg',
64
+ [ServiceType.OPENSEARCH]: 'Analytics/OpenSearch-Service.svg',
65
+ [ServiceType.MSK]: 'Analytics/Managed-Streaming-for-Apache-Kafka.svg',
66
+ [ServiceType.IAM]: 'Security-Identity-Compliance/Identity-and-Access-Management.svg',
67
+ [ServiceType.COGNITO]: 'Security-Identity-Compliance/Cognito.svg',
68
+ [ServiceType.SECRETS_MANAGER]: 'Security-Identity-Compliance/Secrets-Manager.svg',
69
+ [ServiceType.KMS]: 'Security-Identity-Compliance/Key-Management-Service.svg',
70
+ [ServiceType.WAF]: 'Security-Identity-Compliance/WAF.svg',
71
+ [ServiceType.SHIELD]: 'Security-Identity-Compliance/Shield.svg',
72
+ [ServiceType.GUARDDUTY]: 'Security-Identity-Compliance/GuardDuty.svg',
73
+ [ServiceType.INSPECTOR]: 'Security-Identity-Compliance/Inspector.svg',
74
+ [ServiceType.MACIE]: 'Security-Identity-Compliance/Macie.svg',
75
+ [ServiceType.SECURITY_HUB]: 'Security-Identity-Compliance/Security-Hub.svg',
76
+ [ServiceType.CERTIFICATE_MANAGER]: 'Security-Identity-Compliance/Certificate-Manager.svg',
77
+ [ServiceType.FIREWALL_MANAGER]: 'Security-Identity-Compliance/Firewall-Manager.svg',
78
+ [ServiceType.NETWORK_FIREWALL]: 'Security-Identity-Compliance/Network-Firewall.svg',
79
+ [ServiceType.CLOUDWATCH]: 'Management-Governance/CloudWatch.svg',
80
+ [ServiceType.CLOUDTRAIL]: 'Management-Governance/CloudTrail.svg',
81
+ [ServiceType.CONFIG]: 'Management-Governance/Config.svg',
82
+ [ServiceType.SYSTEMS_MANAGER]: 'Management-Governance/Systems-Manager.svg',
83
+ [ServiceType.CLOUDFORMATION]: 'Management-Governance/CloudFormation.svg',
84
+ [ServiceType.ORGANIZATIONS]: 'Management-Governance/Organizations.svg',
85
+ [ServiceType.CONTROL_TOWER]: 'Management-Governance/Control-Tower.svg',
86
+ [ServiceType.TRUSTED_ADVISOR]: 'Management-Governance/Trusted-Advisor.svg',
87
+ [ServiceType.CODECOMMIT]: 'Developer-Tools/CodeCommit.svg',
88
+ [ServiceType.CODEBUILD]: 'Developer-Tools/CodeBuild.svg',
89
+ [ServiceType.CODEDEPLOY]: 'Developer-Tools/CodeDeploy.svg',
90
+ [ServiceType.CODEPIPELINE]: 'Developer-Tools/CodePipeline.svg',
91
+ [ServiceType.CODEARTIFACT]: 'Developer-Tools/CodeArtifact.svg',
92
+ [ServiceType.CLOUD9]: 'Developer-Tools/Cloud9.svg',
93
+ [ServiceType.X_RAY]: 'Developer-Tools/X-Ray.svg',
94
+ [ServiceType.SAGEMAKER]: 'Machine-Learning/SageMaker.svg',
95
+ [ServiceType.COMPREHEND]: 'Machine-Learning/Comprehend.svg',
96
+ [ServiceType.REKOGNITION]: 'Machine-Learning/Rekognition.svg',
97
+ [ServiceType.TEXTRACT]: 'Machine-Learning/Textract.svg',
98
+ [ServiceType.TRANSLATE]: 'Machine-Learning/Translate.svg',
99
+ [ServiceType.TRANSCRIBE]: 'Machine-Learning/Transcribe.svg',
100
+ [ServiceType.POLLY]: 'Machine-Learning/Polly.svg',
101
+ [ServiceType.LEX]: 'Machine-Learning/Lex.svg',
102
+ [ServiceType.DMS]: 'Migration-Transfer/Database-Migration-Service.svg',
103
+ [ServiceType.TRANSFER_FAMILY]: 'Migration-Transfer/Transfer-Family.svg',
104
+ [ServiceType.MIGRATION_HUB]: 'Migration-Transfer/Migration-Hub.svg',
105
+ [ServiceType.DATASYNC]: 'Migration-Transfer/DataSync.svg',
106
+ [ServiceType.SNOWBALL]: 'Migration-Transfer/Snowball.svg',
107
+ [ServiceType.AMPLIFY]: 'Front-End-Web-Mobile/Amplify.svg'
108
+ }
109
+
110
+ export function getAwsIconPath(serviceType?: ServiceType | null): string | null {
111
+ if (!serviceType) {
112
+ return null
113
+ }
114
+ const file = SERVICE_TYPE_TO_ICON[serviceType]
115
+ return file ? `/icons/aws/${file}` : null
116
+ }
117
+
@@ -0,0 +1,73 @@
1
+ import type {GroupConnection, GroupInfo} from '$shared/types'
2
+ import type {FlowEdgeData, FlowGraphInput, FlowNodeData} from '$shared/flow-types'
3
+ import {createGraphSignature, formatConnectionLabel, sanitizeNodeId} from '$lib/utils/flow/helpers'
4
+ import type {Edge, Node} from "@xyflow/svelte"
5
+
6
+ export interface GroupOverviewGraphResult {
7
+ graph: FlowGraphInput | null
8
+ nodeToGroupMap: Record<string, string>
9
+ }
10
+
11
+ export function buildGroupOverviewGraph(
12
+ groups: Record<string, GroupInfo>,
13
+ connections: GroupConnection[] = []
14
+ ): GroupOverviewGraphResult {
15
+ if (!groups || Object.keys(groups).length === 0) {
16
+ return { graph: null, nodeToGroupMap: {} }
17
+ }
18
+
19
+ // Convert each group into a diagram node
20
+ const nodes: Node<FlowNodeData>[] = Object.entries(groups).map(([groupId, groupInfo]) => ({
21
+ id: sanitizeNodeId(groupId, 'mainGroup'),
22
+ type: 'mainGroup',
23
+ data: {
24
+ label: groupInfo.name,
25
+ subLabel: groupInfo.description ?? groupId,
26
+ groupId,
27
+ kind: 'mainGroup'
28
+ },
29
+ width: 120,
30
+ height: 80,
31
+ position: { x: 0, y: 0 },
32
+ }))
33
+
34
+ // Convert each connection into a diagram edge
35
+ const edges: Edge<FlowEdgeData>[] = connections.map((connection, index) => {
36
+ const sourceId = sanitizeNodeId(connection.sourceGroup, 'mainGroup')
37
+ const targetId = sanitizeNodeId(connection.targetGroup, 'mainGroup')
38
+ return {
39
+ id: `edge_${sourceId}_${targetId}_${index}`,
40
+ source: sourceId,
41
+ sourceHandle: 'output',
42
+ target: targetId,
43
+ targetHandle: 'input',
44
+ type: 'snake',
45
+ label: formatConnectionLabel(connection.connectionCount),
46
+ data: {
47
+ label: formatConnectionLabel(connection.connectionCount),
48
+ }
49
+ }
50
+ })
51
+
52
+ // Create a unique signature for the graph for caching/change detection
53
+ const signature = createGraphSignature('group-overview', {
54
+ groups: nodes.map(node => node.data.groupId).sort(),
55
+ connections: connections.map(connection => ({
56
+ sourceGroup: connection.sourceGroup,
57
+ targetGroup: connection.targetGroup,
58
+ connectionCount: connection.connectionCount
59
+ }))
60
+ })
61
+
62
+ const graph: FlowGraphInput = { groupNodes: [], serviceNodes: nodes, edges, signature }
63
+ // Map diagram node IDs back to group IDs for UI interaction
64
+ const nodeToGroupMap = nodes.reduce<Record<string, string>>((lookup, node) => {
65
+ if (node.data.groupId) {
66
+ lookup[node.id] = node.data.groupId
67
+ }
68
+ return lookup
69
+ }, {})
70
+
71
+ console.debug(graph)
72
+ return { graph, nodeToGroupMap }
73
+ }
@@ -0,0 +1,14 @@
1
+ import type {GroupConnection} from '$shared/types'
2
+
3
+ const sanitizeRegEx = /[^a-zA-Z0-9_]/g
4
+
5
+ export const sanitizeNodeId = (value: string, prefix = 'node') => {
6
+ const safe = (value || prefix).replace(sanitizeRegEx, '_')
7
+ return `${prefix}::${safe}`
8
+ }
9
+
10
+ export const formatConnectionLabel = (connectionCount: number) =>
11
+ connectionCount === 1 ? '1 connection' : `${connectionCount} connections`
12
+
13
+ export const createGraphSignature = (namespace: string, payload: unknown) =>
14
+ `${namespace}:${JSON.stringify(payload)}`