@eventcatalog/core 5.0.0-beta.1 → 5.0.0-beta.11

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 (172) hide show
  1. package/LICENSE +227 -128
  2. package/README.md +4 -4
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +71 -35
  8. package/dist/analytics/log-build.js +5 -4
  9. package/dist/analytics/telemetry.cjs +42 -0
  10. package/dist/analytics/telemetry.d.cts +20 -0
  11. package/dist/analytics/telemetry.d.ts +20 -0
  12. package/dist/analytics/telemetry.js +8 -0
  13. package/dist/{chunk-KYJZJLBD.js → chunk-4YFM63DS.js} +9 -3
  14. package/dist/{chunk-UDS3SYLZ.js → chunk-76DAAJLV.js} +56 -33
  15. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  16. package/dist/chunk-CPW6G62V.js +17 -0
  17. package/dist/{chunk-K2ACDO7Y.js → chunk-LQZ7IG7B.js} +1 -1
  18. package/dist/{chunk-S2QGT7ZF.js → chunk-MFEMKQZ6.js} +1 -1
  19. package/dist/{chunk-Q6SD5SNQ.js → chunk-WM4QYMIA.js} +1 -1
  20. package/dist/constants.cjs +1 -1
  21. package/dist/constants.js +1 -1
  22. package/dist/eventcatalog.cjs +204 -124
  23. package/dist/eventcatalog.config.d.cts +59 -1
  24. package/dist/eventcatalog.config.d.ts +59 -1
  25. package/dist/eventcatalog.js +30 -11
  26. package/dist/federation/federate.js +2 -2
  27. package/dist/generate.cjs +1 -1
  28. package/dist/generate.js +3 -3
  29. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  30. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  31. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  32. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  33. package/dist/runtime-dependencies/manifest.json +8 -1
  34. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  35. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  36. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  37. package/dist/runtime-dependencies/yjs.mjs +3 -0
  38. package/dist/utils/cli-logger.cjs +1 -1
  39. package/dist/utils/cli-logger.js +2 -2
  40. package/dist/utils/license-status.cjs +71 -45
  41. package/dist/utils/license-status.d.cts +14 -5
  42. package/dist/utils/license-status.d.ts +14 -5
  43. package/dist/utils/license-status.js +1 -1
  44. package/eventcatalog/astro.config.mjs +5 -0
  45. package/eventcatalog/integrations/storage.mjs +20 -0
  46. package/eventcatalog/integrations/studio-server.mjs +32 -0
  47. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  48. package/eventcatalog/src/components/Header.astro +3 -3
  49. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  50. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  51. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  52. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  53. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  54. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  55. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  56. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  57. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  58. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  59. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  60. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  61. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  62. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  63. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
  64. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
  65. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  66. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  67. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  68. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  69. package/eventcatalog/src/features/server/start.ts +25 -0
  70. package/eventcatalog/src/features/storage/database.ts +88 -0
  71. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  72. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  73. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  74. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  75. package/eventcatalog/src/features/storage/sql.ts +28 -0
  76. package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
  77. package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
  78. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  79. package/eventcatalog/src/features/studio/api/client.ts +35 -0
  80. package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
  81. package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
  82. package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
  83. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  84. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  85. package/eventcatalog/src/features/studio/clipboard.ts +97 -0
  86. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
  87. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
  88. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
  89. package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
  90. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
  91. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
  92. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  93. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
  94. package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
  95. package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
  96. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
  97. package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
  98. package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
  99. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
  100. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
  101. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
  102. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  103. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
  104. package/eventcatalog/src/features/studio/components/icons.ts +53 -0
  105. package/eventcatalog/src/features/studio/components/status.ts +53 -0
  106. package/eventcatalog/src/features/studio/edges.ts +56 -0
  107. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  108. package/eventcatalog/src/features/studio/grouping.ts +226 -0
  109. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  110. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  111. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
  112. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  113. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
  114. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
  115. package/eventcatalog/src/features/studio/identity.ts +37 -0
  116. package/eventcatalog/src/features/studio/layout.ts +42 -0
  117. package/eventcatalog/src/features/studio/levels.ts +282 -0
  118. package/eventcatalog/src/features/studio/node-types.ts +205 -0
  119. package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
  120. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  121. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  122. package/eventcatalog/src/features/studio/placement.ts +197 -0
  123. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
  124. package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
  125. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  126. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  127. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  128. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  129. package/eventcatalog/src/features/studio/studio.css +273 -0
  130. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  131. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
  132. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  133. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  134. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  135. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  136. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  137. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  138. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  139. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  140. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  141. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  142. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  143. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  144. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  145. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  146. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  147. package/eventcatalog/src/styles/tailwind.css +2 -2
  148. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  149. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  150. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  151. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  152. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  153. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  154. package/eventcatalog/src/utils/collections/util.ts +31 -0
  155. package/eventcatalog/src/utils/feature.ts +8 -0
  156. package/eventcatalog/src/utils/git.ts +11 -5
  157. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  158. package/eventcatalog/src/utils/trial.ts +1 -1
  159. package/package.json +17 -12
  160. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  161. package/eventcatalog/public/studio-bg.png +0 -0
  162. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  163. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  164. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  165. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  166. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  167. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  168. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  169. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  170. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  171. package/eventcatalog/src/pages/studio.astro +0 -316
  172. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -0,0 +1,282 @@
1
+ import { MarkerType, type Edge, type Node } from '@xyflow/react';
2
+ import { getMessagesLabel } from '@eventcatalog/visualiser/layout';
3
+ import { MESSAGE_COLLECTIONS } from './edges';
4
+ import { GROUP_TYPES, isGroupType, isWrittenOnCanvas } from './node-types';
5
+
6
+ /**
7
+ * Levels of detail, the same as EventCatalog's diagrams:
8
+ * - L1: domains, systems and how they relate (services folded into the system or domain that contains them)
9
+ * - L2: services and data stores, with messages and channels hidden (the nodes either side connected directly)
10
+ * - L3: everything, including messages and channels (the canvas people edit)
11
+ *
12
+ * What a domain or system contains is what sits inside it (as a container), or what it's connected to with
13
+ * "contains" (as a card, which catalog domains and systems get when their services are on the canvas).
14
+ */
15
+ export type Level = 1 | 2 | 3;
16
+
17
+ export const LEVELS: { level: Level; description: string }[] = [
18
+ { level: 1, description: 'Domains, systems and their relationships' },
19
+ { level: 2, description: 'Services and data stores' },
20
+ { level: 3, description: 'Everything, including messages and channels' },
21
+ ];
22
+
23
+ // Domains and systems: as cards (what they contain is connected with "contains") or containers (it's inside)
24
+ const CONTAINER_TYPES = new Set(['system', 'context-domain', GROUP_TYPES.system, GROUP_TYPES.domain]);
25
+ const isSystem = (node: Node) => node.type === 'system' || node.type === GROUP_TYPES.system;
26
+ const isDomain = (node: Node) => node.type === 'context-domain' || node.type === GROUP_TYPES.domain;
27
+
28
+ const isMessage = (node: Node) => !!MESSAGE_COLLECTIONS[node.type ?? ''];
29
+ const isCarrier = (node: Node) => isMessage(node) || node.type === 'channel';
30
+ const isContainer = (node: Node) => CONTAINER_TYPES.has(node.type ?? '');
31
+ const messageName = (node: Node) => String((node.data as { message?: { name?: string } }).message?.name ?? node.id);
32
+
33
+ // Edges as the visualiser's levels draw them: what's folded together is joined by a plain line, and what was joined
34
+ // through hidden messages or channels by a dashed, muted one ("bridged"), each with the visualiser's arrow
35
+ const EDGE_STROKE = 'var(--ec-edge-stroke, #6b7280)';
36
+ const MUTED = 'rgb(var(--ec-page-text-muted))';
37
+ const FOLDED_EDGE = {
38
+ type: 'smoothstep',
39
+ style: { strokeWidth: 1, stroke: EDGE_STROKE },
40
+ markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
41
+ } satisfies Partial<Edge>;
42
+ const BRIDGED_EDGE = {
43
+ type: 'smoothstep',
44
+ style: { strokeWidth: 1.5, stroke: MUTED, strokeDasharray: '5 5' },
45
+ markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, color: MUTED },
46
+ } satisfies Partial<Edge>;
47
+
48
+ /** What a node becomes at a level: kept (as itself or another node), carried through (messages, channels) or dropped */
49
+ type Representative = { kind: 'node'; id: string } | { kind: 'carrier' } | { kind: 'drop' };
50
+
51
+ /**
52
+ * Collapse a graph: each node is kept, replaced by another node (e.g. a service by its system), carried through
53
+ * (messages and channels: the nodes either side get connected directly, "bridged") or dropped. Edges between two
54
+ * nodes that are kept as they are stay as they were (`keepDirect`, for L2); the rest are merged per pair and kind
55
+ * (folded or bridged), labelled with the messages they carry.
56
+ */
57
+ const collapseGraph = (
58
+ nodes: Node[],
59
+ edges: Edge[],
60
+ representativeOf: (node: Node) => Representative,
61
+ { keepDirect = false }: { keepDirect?: boolean } = {}
62
+ ) => {
63
+ const byId = new Map(nodes.map((node) => [node.id, node]));
64
+ // Whether one node is inside another (e.g. a system in its domain): they're not connected, one holds the other
65
+ const isInside = (id: string, containerId: string) => {
66
+ for (let node = byId.get(id); node?.parentId; node = byId.get(node.parentId)) if (node.parentId === containerId) return true;
67
+ return false;
68
+ };
69
+ const representatives = new Map(nodes.map((node) => [node.id, representativeOf(node)]));
70
+ const outgoing = new Map<string, Edge[]>();
71
+ edges.forEach((edge) => outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge]));
72
+
73
+ type Merged = { source: string; target: string; bridged: boolean; messages: Map<string, string>; labels: string[] };
74
+ const merged = new Map<string, Merged>();
75
+ const kept: Edge[] = [];
76
+ const addMessage = (messages: Map<string, string>, node: Node) => {
77
+ if (isMessage(node)) messages.set(messageName(node), MESSAGE_COLLECTIONS[node.type!]);
78
+ };
79
+
80
+ edges.forEach((edge) => {
81
+ const from = representatives.get(edge.source);
82
+ if (from?.kind !== 'node') return;
83
+ const to = representatives.get(edge.target);
84
+ if (keepDirect && from.id === edge.source && to?.kind === 'node' && to.id === edge.target) return kept.push(edge);
85
+ // Follow the edge through carried nodes to the node(s) it reaches, collecting the messages on the way
86
+ const stack = [{ id: edge.target, messages: new Map<string, string>(), labels: [String(edge.label ?? '')], bridged: false }];
87
+ const visited = new Set<string>();
88
+ addMessage(stack[0].messages, byId.get(edge.source)!);
89
+ while (stack.length) {
90
+ const current = stack.pop()!;
91
+ if (visited.has(current.id)) continue;
92
+ visited.add(current.id);
93
+ const node = byId.get(current.id);
94
+ const reached = representatives.get(current.id);
95
+ if (!node || !reached || reached.kind === 'drop') continue;
96
+ const messages = new Map(current.messages);
97
+ addMessage(messages, node);
98
+ if (reached.kind === 'carrier') {
99
+ (outgoing.get(current.id) ?? []).forEach((next) =>
100
+ stack.push({ id: next.target, messages, labels: [] as string[], bridged: true })
101
+ );
102
+ continue;
103
+ }
104
+ // Edges inside what was collapsed disappear, and so do edges to what a node is in (e.g. a service loose in a
105
+ // domain, folded into it, and a system in the same domain)
106
+ if (from.id === reached.id || isInside(from.id, reached.id) || isInside(reached.id, from.id)) continue;
107
+ const key = `${current.bridged ? 'bridged' : 'level'}-${from.id}-${reached.id}`;
108
+ const entry: Merged = merged.get(key) ?? {
109
+ source: from.id,
110
+ target: reached.id,
111
+ bridged: current.bridged,
112
+ messages: new Map(),
113
+ labels: [],
114
+ };
115
+ messages.forEach((collection, name) => entry.messages.set(name, collection));
116
+ entry.labels.push(...current.labels.filter(Boolean));
117
+ merged.set(key, entry);
118
+ }
119
+ });
120
+
121
+ const keptIds = new Set([...representatives.values()].flatMap((rep) => (rep.kind === 'node' ? [rep.id] : [])));
122
+ // Actors' connections are relationships, labelled as they were (like the visualiser's context relationships)
123
+ const isActor = (id: string) => byId.get(id)?.type === 'actor';
124
+ return {
125
+ nodes: nodes.filter((node) => keptIds.has(node.id)),
126
+ edges: [
127
+ ...kept,
128
+ ...[...merged.entries()].map(([id, { source, target, bridged, messages, labels }]): Edge => {
129
+ const label =
130
+ getMessagesLabel(messages) ??
131
+ (!bridged && (isActor(source) || isActor(target)) && labels.length ? [...new Set(labels)].join(', ') : undefined);
132
+ return { id, source, target, ...(bridged ? BRIDGED_EDGE : FOLDED_EDGE), ...(label && { label }) };
133
+ }),
134
+ ],
135
+ };
136
+ };
137
+
138
+ /**
139
+ * The system and domain each node is in: the containers it sits inside, or the domain and system cards
140
+ * connected to it with "contains"
141
+ */
142
+ const getContainers = (nodes: Node[], edges: Edge[]) => {
143
+ const byId = new Map(nodes.map((node) => [node.id, node]));
144
+ const containers = new Map<string, { system?: string; domain?: string }>();
145
+ const add = (nodeId: string, container: Node) => {
146
+ const entry = containers.get(nodeId) ?? {};
147
+ if (isSystem(container)) entry.system ??= container.id;
148
+ else entry.domain ??= container.id;
149
+ containers.set(nodeId, entry);
150
+ };
151
+ for (const node of nodes) {
152
+ for (
153
+ let parent = node.parentId ? byId.get(node.parentId) : undefined;
154
+ parent;
155
+ parent = parent.parentId ? byId.get(parent.parentId) : undefined
156
+ ) {
157
+ if (isContainer(parent)) add(node.id, parent);
158
+ }
159
+ }
160
+ for (const edge of edges) {
161
+ const container = byId.get(edge.source);
162
+ if (container && isContainer(container) && edge.label === 'contains') add(edge.target, container);
163
+ }
164
+ return containers;
165
+ };
166
+
167
+ /**
168
+ * Containers are sized by the layout: to fit what's in them, or (empty) the size the visualiser lays empty groups
169
+ * out at. Same ids, so switching levels grows and shrinks them.
170
+ */
171
+ const sizedByLayout = ({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) => ({
172
+ edges,
173
+ nodes: nodes.map((node) =>
174
+ isGroupType(node.type) ? { ...node, width: undefined, height: undefined, measured: undefined } : node
175
+ ),
176
+ });
177
+
178
+ /** L2: messages and channels hidden, the nodes either side connected directly */
179
+ const level2 = (nodes: Node[], edges: Edge[]) =>
180
+ collapseGraph(nodes, edges, (node) => (isCarrier(node) ? { kind: 'carrier' } : { kind: 'node', id: node.id }), {
181
+ keepDirect: true,
182
+ });
183
+
184
+ /** Whether a node is inside a container, at any depth */
185
+ const isWithin = (node: Node, containerId: string, byId: Map<string, Node>) => {
186
+ for (let parent = node.parentId; parent; parent = byId.get(parent)?.parentId) if (parent === containerId) return true;
187
+ return false;
188
+ };
189
+
190
+ /**
191
+ * L1 as EventCatalog's diagrams draw it: systems as cards with what's in them counted, and domains with no system in
192
+ * them (or in their subdomains) as domain cards
193
+ */
194
+ const asSystemsLevel = (
195
+ { nodes, edges }: { nodes: Node[]; edges: Edge[] },
196
+ { all, allEdges, containers }: { all: Node[]; allEdges: Edge[]; containers: ReturnType<typeof getContainers> }
197
+ ) => {
198
+ const byId = new Map(all.map((node) => [node.id, node]));
199
+ const count = (id: string, matches: (node: Node) => boolean) =>
200
+ all.filter((node) => matches(node) && containers.get(node.id)?.system === id).length;
201
+ // The messages a system's services send and receive
202
+ const messagesOf = (id: string) => {
203
+ const services = new Set(all.filter((node) => containers.get(node.id)?.system === id).map((node) => node.id));
204
+ const messages = new Set<string>();
205
+ for (const edge of allEdges) {
206
+ const [inside, other] = services.has(edge.source) ? [edge.source, edge.target] : [edge.target, edge.source];
207
+ if (services.has(inside) && byId.get(other) && isMessage(byId.get(other)!)) messages.add(other);
208
+ }
209
+ return messages.size;
210
+ };
211
+ return {
212
+ edges,
213
+ nodes: nodes.map((node): Node => {
214
+ const { id, parentId, position, data } = node;
215
+ if (node.type === GROUP_TYPES.system)
216
+ return {
217
+ id,
218
+ type: 'system',
219
+ position,
220
+ ...(parentId && { parentId }),
221
+ data: {
222
+ mode: 'full',
223
+ ...data,
224
+ servicesCount: count(id, (child) => child.type === 'service' || child.type === 'agent'),
225
+ containersCount: count(id, (child) => child.type === 'data'),
226
+ messagesCount: messagesOf(id),
227
+ },
228
+ };
229
+ if (node.type === GROUP_TYPES.domain && !all.some((other) => isSystem(other) && isWithin(other, id, byId)))
230
+ return {
231
+ id,
232
+ type: 'context-domain',
233
+ position,
234
+ ...(parentId && { parentId }),
235
+ data: {
236
+ mode: 'full',
237
+ ...data,
238
+ subdomain: !!parentId,
239
+ systemsCount: 0,
240
+ servicesCount: all.filter((other) => other.type === 'service' && isWithin(other, id, byId)).length,
241
+ },
242
+ };
243
+ return node;
244
+ }),
245
+ };
246
+ };
247
+
248
+ /** L1: services folded into their system (or domain); actors, external systems and loose services stay */
249
+ const level1 = (nodes: Node[], edges: Edge[]) => {
250
+ const containers = getContainers(nodes, edges);
251
+ const collapsed = collapseGraph(nodes, edges, (node) => {
252
+ const container = containers.get(node.id);
253
+ // Systems stay (inside their domain's container, if they're in one); everything else folds into its system or domain
254
+ if (isContainer(node) && (isSystem(node) || !container?.system)) return { kind: 'node', id: node.id };
255
+ if (container?.system) return { kind: 'node', id: container.system };
256
+ // Messages and channels connect the systems either side, even when they sit in a domain rather than a system
257
+ // (the visualiser has them outside its domains; catalog domains on a canvas hold their messages)
258
+ if (isCarrier(node)) return { kind: 'carrier' };
259
+ if (container?.domain) return { kind: 'node', id: container.domain };
260
+ if (node.type === 'data' || isWrittenOnCanvas(node.type)) return { kind: 'drop' };
261
+ return { kind: 'node', id: node.id };
262
+ });
263
+ return asSystemsLevel(collapsed, { all: nodes, allEdges: edges, containers });
264
+ };
265
+
266
+ /**
267
+ * The graph for a level (not laid out: positions come from the layout). Notes aren't in it: the ones added on the
268
+ * level are shown where they were put, over its layout.
269
+ */
270
+ export const getLevelGraph = (nodes: Node[], edges: Edge[], level: Level) => {
271
+ if (level === 3) return { nodes, edges };
272
+ const withoutNotes = nodes.filter((node) => node.type !== 'note');
273
+ return sizedByLayout(level === 1 ? level1(withoutNotes, edges) : level2(withoutNotes, edges));
274
+ };
275
+
276
+ /** Why a level can't be shown for this canvas, if it can't */
277
+ export const getLevelUnavailableReason = (nodes: Node[], level: Level): string | undefined => {
278
+ // Like the visualiser: L1 needs systems, or domains to relate (one domain on its own has nothing to show)
279
+ if (level === 1 && !nodes.some(isSystem) && nodes.filter(isDomain).length < 2)
280
+ return 'Not available: add a system, or more than one domain, to see how they relate';
281
+ return undefined;
282
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * The kinds of node people (and agents) can put on a canvas, and what's in their data. Shared by the browser and
3
+ * the server, so it has no UI in it: the icons for each type are in components/icons.ts.
4
+ */
5
+
6
+ export type NodeCategory = 'boundaries' | 'messages' | 'architecture' | 'other';
7
+
8
+ export const NODE_CATEGORIES: { id: NodeCategory; label: string }[] = [
9
+ { id: 'boundaries', label: 'Boundaries' },
10
+ { id: 'messages', label: 'Messages' },
11
+ { id: 'architecture', label: 'Architecture' },
12
+ { id: 'other', label: 'Other' },
13
+ ];
14
+
15
+ /** Domains and systems as containers: boxes other nodes sit in */
16
+ export const GROUP_TYPES = { domain: 'domain-group', system: 'system-group' } as const;
17
+ export const isGroupType = (type?: string) => type === GROUP_TYPES.domain || type === GROUP_TYPES.system;
18
+
19
+ export type NodeDefinition = {
20
+ /** React Flow node type, matches the visualiser node component keys */
21
+ type: string;
22
+ label: string;
23
+ category: NodeCategory;
24
+ /** Not in the components list (still drawn, for canvases that have them) */
25
+ unlisted?: boolean;
26
+ /** Where the resource lives inside node.data (e.g. `service` for data.service). Undefined = data root */
27
+ resourceKey?: string;
28
+ /** Nodes that are sized on the canvas (containers, notes, text) start at this size */
29
+ defaultSize?: { width: number; height: number };
30
+ /** Only its width is set: its height follows what's in it (text) */
31
+ autoHeight?: boolean;
32
+ createData: () => Record<string, unknown>;
33
+ };
34
+
35
+ const resource = (name: string, summary: string) => ({ name, version: '0.0.1', summary, owners: [] });
36
+
37
+ export const nodeDefinitions: NodeDefinition[] = [
38
+ {
39
+ type: GROUP_TYPES.domain,
40
+ label: 'Domain',
41
+ category: 'boundaries',
42
+ resourceKey: 'domain',
43
+ defaultSize: { width: 720, height: 460 },
44
+ createData: () => ({ domain: { name: 'New Domain', version: '0.0.1' } }),
45
+ },
46
+ {
47
+ type: GROUP_TYPES.system,
48
+ label: 'System',
49
+ category: 'boundaries',
50
+ resourceKey: 'system',
51
+ defaultSize: { width: 560, height: 360 },
52
+ createData: () => ({ system: { name: 'New System', version: '0.0.1' } }),
53
+ },
54
+ {
55
+ type: 'service',
56
+ category: 'architecture',
57
+ label: 'Service',
58
+ resourceKey: 'service',
59
+ createData: () => ({
60
+ mode: 'full',
61
+ service: { ...resource('New Service', 'Describe what this service does.'), sends: [], receives: [] },
62
+ }),
63
+ },
64
+ {
65
+ type: 'event',
66
+ category: 'messages',
67
+ label: 'Event',
68
+ resourceKey: 'message',
69
+ createData: () => ({ mode: 'full', message: resource('NewEvent', 'Something happened.') }),
70
+ },
71
+ {
72
+ type: 'command',
73
+ category: 'messages',
74
+ label: 'Command',
75
+ resourceKey: 'message',
76
+ createData: () => ({ mode: 'full', message: resource('NewCommand', 'Asks a service to do something.') }),
77
+ },
78
+ {
79
+ type: 'query',
80
+ category: 'messages',
81
+ label: 'Query',
82
+ resourceKey: 'message',
83
+ createData: () => ({ mode: 'full', message: resource('NewQuery', 'Asks for some information.') }),
84
+ },
85
+ {
86
+ type: 'channel',
87
+ category: 'architecture',
88
+ label: 'Channel',
89
+ resourceKey: 'channel',
90
+ createData: () => ({ mode: 'full', channel: { ...resource('new.channel', 'Where messages are routed.'), protocols: [] } }),
91
+ },
92
+ {
93
+ type: 'data',
94
+ category: 'architecture',
95
+ label: 'Data Store',
96
+ resourceKey: 'data',
97
+ createData: () => ({ mode: 'full', data: { ...resource('New Database', 'Stores data.'), container_type: 'database' } }),
98
+ },
99
+ {
100
+ type: 'view',
101
+ category: 'architecture',
102
+ label: 'View',
103
+ resourceKey: 'view',
104
+ createData: () => ({ mode: 'full', view: resource('New View', 'What the user sees.') }),
105
+ },
106
+ {
107
+ type: 'externalSystem',
108
+ category: 'architecture',
109
+ unlisted: true,
110
+ label: 'External System',
111
+ resourceKey: 'externalSystem',
112
+ createData: () => ({ mode: 'full', externalSystem: resource('External System', 'A third party system.') }),
113
+ },
114
+ {
115
+ type: 'agent',
116
+ category: 'architecture',
117
+ label: 'Agent',
118
+ resourceKey: 'agent',
119
+ createData: () => ({ mode: 'full', agent: { ...resource('New Agent', 'What this agent does.'), sends: [], receives: [] } }),
120
+ },
121
+ {
122
+ type: 'actor',
123
+ category: 'other',
124
+ label: 'Actor',
125
+ createData: () => ({ mode: 'full', name: 'Customer', summary: 'A person using the system.' }),
126
+ },
127
+ {
128
+ type: 'text',
129
+ category: 'other',
130
+ label: 'Text',
131
+ defaultSize: { width: 240, height: 40 },
132
+ autoHeight: true,
133
+ createData: () => ({ text: 'Add some text' }),
134
+ },
135
+ {
136
+ type: 'note',
137
+ category: 'other',
138
+ label: 'Sticky Note',
139
+ // About square, like a post-it (taller if more is written on it)
140
+ defaultSize: { width: 200, height: 176 },
141
+ autoHeight: true,
142
+ createData: () => ({ text: '', color: 'yellow' }),
143
+ },
144
+ ];
145
+
146
+ /** The types people and agents can add (not the unlisted ones) */
147
+ export const COMPONENT_TYPES = nodeDefinitions
148
+ .filter((definition) => !definition.unlisted)
149
+ .map((definition) => definition.type) as [string, ...string[]];
150
+
151
+ const definitionsByType = new Map(nodeDefinitions.map((definition) => [definition.type, definition]));
152
+
153
+ // Node types that only come from the catalog (not in the components list)
154
+ const catalogNodeTypes: Record<string, { label: string; resourceKey: string }> = {
155
+ system: { label: 'System', resourceKey: 'system' },
156
+ 'context-domain': { label: 'Domain', resourceKey: 'domain' },
157
+ };
158
+
159
+ export const getNodeDefinition = (type?: string) => (type ? definitionsByType.get(type) : undefined);
160
+
161
+ export const getNodeLabel = (type?: string) => getNodeDefinition(type)?.label ?? catalogNodeTypes[type ?? '']?.label ?? 'Node';
162
+
163
+ // About the size the visualiser's resource cards render at
164
+ export const DEFAULT_NODE_SIZE = { width: 240, height: 112 };
165
+
166
+ /** The size a new node of a type starts at (or, for cards, about the size it renders at) */
167
+ export const getNodeSize = (type?: string) => getNodeDefinition(type)?.defaultSize ?? DEFAULT_NODE_SIZE;
168
+
169
+ const getResourceKey = (type?: string) => getNodeDefinition(type)?.resourceKey ?? catalogNodeTypes[type ?? '']?.resourceKey;
170
+
171
+ /** The resource object for a node (e.g. data.service), or the data itself for flat nodes */
172
+ export const getResource = (type: string | undefined, data: Record<string, unknown>): Record<string, unknown> => {
173
+ const key = getResourceKey(type);
174
+ return key ? ((data[key] as Record<string, unknown>) ?? {}) : data;
175
+ };
176
+
177
+ /** Sticky notes sit on the canvas wherever they're put, never inside a container */
178
+ export const canGoInContainer = (type?: string) => type !== 'note';
179
+
180
+ /**
181
+ * The level (L1, L2 or L3) a sticky note was added on: it's only shown there. Notes from before levels had notes
182
+ * are on L3, the canvas people edit. Other nodes are on every level they fit in, so they have none.
183
+ */
184
+ export const getNoteLevel = (node: { type?: string; data: Record<string, unknown> }): 1 | 2 | 3 | undefined =>
185
+ node.type === 'note' ? ((node.data.level as 1 | 2 | undefined) ?? 3) : undefined;
186
+
187
+ /** Whether a node is on a level's canvas as itself: everything on L3, but notes only on the level they were added on */
188
+ export const isOnLevel = (node: { type?: string; data: Record<string, unknown> }, level: 1 | 2 | 3) =>
189
+ node.type !== 'note' || getNoteLevel(node) === level;
190
+
191
+ /** Notes and text: written on the canvas itself (their text is their data, not a resource with a name) */
192
+ export const isWrittenOnCanvas = (type?: string) => type === 'note' || type === 'text';
193
+
194
+ /** Whether a node's resource has a version (actors and notes don't) */
195
+ export const isVersioned = (type?: string) => !!getResourceKey(type);
196
+
197
+ /** Returns new node data with fields of the resource changed */
198
+ export const updateResource = (type: string | undefined, data: Record<string, unknown>, fields: Record<string, unknown>) => {
199
+ const key = getResourceKey(type);
200
+ return key ? { ...data, [key]: { ...getResource(type, data), ...fields } } : { ...data, ...fields };
201
+ };
202
+
203
+ /** A node's name, as people and agents see it */
204
+ export const getNodeName = (type: string | undefined, data: Record<string, unknown>, fallback = 'node') =>
205
+ String(getResource(type, data).name ?? data.text ?? fallback);
@@ -0,0 +1,68 @@
1
+ ---
2
+ // Visualiser styles ship in the page head so ClientRouter navigations keep them (see NodeGraph.astro).
3
+ import '@eventcatalog/visualiser/styles-core.css';
4
+ import '../studio.css';
5
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
6
+ import { buildUrl } from '@utils/url-builder';
7
+ import StudioDesigner from '../components/StudioDesigner';
8
+ import StudioNotice from '../components/StudioNotice.astro';
9
+ import { getCatalogResources } from '../catalog-resources';
10
+ import { startStudio, STUDIO_SOCKET_PATH } from '../server/runtime';
11
+ import { getSignedInUser, isSignedIn } from '../server/sign-in';
12
+ import { CANVASES_API_PATH } from '../api/canvases-api';
13
+
14
+ export const prerender = false;
15
+
16
+ const { id } = Astro.params;
17
+
18
+ if (!id || !/^[0-9a-f-]{36}$/i.test(id)) {
19
+ return Astro.redirect(buildUrl('/studio'));
20
+ }
21
+
22
+ // Studio can't start when its storage can't be used (e.g. the database was updated by a newer EventCatalog)
23
+ const started = await startStudio({ isSignedIn }).catch((error: Error) => error);
24
+ const unavailable = started instanceof Error ? started.message : undefined;
25
+ const runtime = started instanceof Error ? undefined : started;
26
+ // A deleted canvas's link (anyone who still had it open was told)
27
+ const deleted = runtime?.isDeleted(id) ?? false;
28
+ if (unavailable) Astro.response.status = 503;
29
+ else if (deleted) Astro.response.status = 410;
30
+ const { resources, relations } = runtime && !deleted ? await getCatalogResources() : { resources: [], relations: [] };
31
+ const keptInMemory = runtime?.storage.type === 'memory';
32
+ const signedInAs = getSignedInUser(Astro.locals);
33
+ ---
34
+
35
+ <VerticalSideBarLayout title="Studio - EventCatalog" showNestedSideBar={false}>
36
+ <main
37
+ class="min-h-0 overflow-hidden bg-[rgb(var(--ec-page-bg))]"
38
+ style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1); height: calc(100dvh - 60px);"
39
+ >
40
+ {
41
+ unavailable ? (
42
+ <StudioNotice title="Studio isn't available" message={unavailable} />
43
+ ) : deleted ? (
44
+ <StudioNotice
45
+ title="This canvas was deleted"
46
+ message="Someone deleted it, so it can't be opened any more."
47
+ href={buildUrl('/studio')}
48
+ linkText="Back to Studio"
49
+ />
50
+ ) : (
51
+ <StudioDesigner
52
+ client:only="react"
53
+ canvasId={id}
54
+ socketPath={buildUrl(STUDIO_SOCKET_PATH, true)}
55
+ newCanvasUrl={`${buildUrl('/studio')}?new=1`}
56
+ studioUrl={buildUrl('/studio')}
57
+ canvasesApiUrl={buildUrl(CANVASES_API_PATH, true)}
58
+ mcpUrl={buildUrl('/docs/mcp', true)}
59
+ relayScriptPath={buildUrl('/webmcp-relay/embed.js', true)}
60
+ keptInMemory={keptInMemory}
61
+ signedInAs={signedInAs}
62
+ resources={resources}
63
+ relations={relations}
64
+ />
65
+ )
66
+ }
67
+ </main>
68
+ </VerticalSideBarLayout>