@eventcatalog/core 5.4.1 → 5.5.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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-GQQFHJSY.js → chunk-3TREOJOK.js} +1 -1
- package/dist/{chunk-WG276AIE.js → chunk-754U4T7K.js} +1 -1
- package/dist/{chunk-GAKU752C.js → chunk-RDF2OMEF.js} +1 -1
- package/dist/{chunk-VZNPE26F.js → chunk-UBDTKNFS.js} +1 -1
- package/dist/{chunk-NSVRGATQ.js → chunk-WPFA7AGD.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +8 -1
- package/eventcatalog/src/features/ai/chat-api.ts +6 -3
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/shared.ts +3 -1
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/view.tsx +6 -1
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +78 -66
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +85 -71
- package/eventcatalog/src/features/mcp/mcp-scoped-tools.ts +2 -1
- package/eventcatalog/src/features/mcp/mcp-server.ts +9 -8
- package/eventcatalog/src/features/studio/api/canvases-api.ts +3 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +8 -2
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2 -0
- package/eventcatalog/src/features/tools/catalog-tools.ts +8 -1
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/_mermaid-route.ts +48 -5
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/[level].mermaid.ts +18 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +1 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/[level].mermaid.ts +18 -0
- package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/utils/node-graphs/architecture-diagram-types.ts +7 -1
- package/eventcatalog/src/utils/node-graphs/architecture-diagram.ts +5 -2
- package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +2 -0
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +246 -29
- package/eventcatalog/src/utils/node-graphs/system-levels-node-graph.ts +195 -9
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +48 -5
- package/package.json +4 -4
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -375
|
@@ -15,6 +15,11 @@ interface NodesAndEdgesProps {
|
|
|
15
15
|
wrapInSystemGroup?: boolean;
|
|
16
16
|
channelRenderMode?: 'single' | 'flat';
|
|
17
17
|
layout?: boolean;
|
|
18
|
+
// Only parent this system's own services. Services pulled in because they
|
|
19
|
+
// send or receive the same messages stay in the graph, but outside the group.
|
|
20
|
+
// The domain diagram leaves this off: it uses the parent to tell which
|
|
21
|
+
// system's graph a service came from.
|
|
22
|
+
confineToMembers?: boolean;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const getNodesAndEdges = async ({
|
|
@@ -25,6 +30,7 @@ export const getNodesAndEdges = async ({
|
|
|
25
30
|
wrapInSystemGroup = false,
|
|
26
31
|
channelRenderMode = 'flat',
|
|
27
32
|
layout = true,
|
|
33
|
+
confineToMembers = false,
|
|
28
34
|
}: NodesAndEdgesProps) => {
|
|
29
35
|
let nodes = new Map(),
|
|
30
36
|
edges = new Map();
|
|
@@ -118,21 +124,52 @@ export const getNodesAndEdges = async ({
|
|
|
118
124
|
});
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
// Wrap
|
|
122
|
-
//
|
|
127
|
+
// Wrap the graph in one parent "system group" boundary node. Messages,
|
|
128
|
+
// channels and data stores stay in it. With confineToMembers, only this
|
|
129
|
+
// system's own services are children; other services are left outside.
|
|
123
130
|
const graph = {
|
|
124
|
-
nodes:
|
|
131
|
+
nodes:
|
|
132
|
+
wrapInSystemGroup && nodes.size > 0
|
|
133
|
+
? wrapNodesInSystemGroup(
|
|
134
|
+
[...nodes.values()],
|
|
135
|
+
system,
|
|
136
|
+
confineToMembers ? memberServiceNodeIds(system, serviceMap) : undefined
|
|
137
|
+
)
|
|
138
|
+
: [...nodes.values()],
|
|
125
139
|
edges: [...edges.values()],
|
|
126
140
|
};
|
|
127
141
|
|
|
128
142
|
return layout ? layoutNodeGraph(graph) : graph;
|
|
129
143
|
};
|
|
130
144
|
|
|
145
|
+
/** Service and agent nodes. Anything else in the graph (messages, channels, data stores) stays with the system. */
|
|
146
|
+
export const SERVICE_NODE_TYPES = new Set(['services', 'agents']);
|
|
147
|
+
|
|
148
|
+
/** Node ids of the services a system lists, with `latest` and semver ranges resolved to a real version. */
|
|
149
|
+
export const memberServiceNodeIds = (
|
|
150
|
+
system: { data: { services?: { id: string; version?: string }[] } },
|
|
151
|
+
serviceMap: Map<string, { data: { id: string; version?: string } }[]>
|
|
152
|
+
) => {
|
|
153
|
+
const ids = new Set<string>();
|
|
154
|
+
for (const ref of system.data.services || []) {
|
|
155
|
+
const service = findInMap(serviceMap, ref.id, ref.version);
|
|
156
|
+
if (service) ids.add(`${service.data.id}-${service.data.version}`);
|
|
157
|
+
}
|
|
158
|
+
return ids;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const withoutGroupParent = (node: any) => {
|
|
162
|
+
const { parentId, extent, ...rest } = node;
|
|
163
|
+
return rest;
|
|
164
|
+
};
|
|
165
|
+
|
|
131
166
|
/**
|
|
132
167
|
* Wraps a system's nodes in one parent `system-group` node (sized to fit them
|
|
133
168
|
* when the graph is laid out), so they visibly belong to the system.
|
|
169
|
+
* When `memberIds` is set, service and agent nodes that aren't members are
|
|
170
|
+
* left outside the group.
|
|
134
171
|
*/
|
|
135
|
-
const wrapNodesInSystemGroup = (systemNodes: any[], system: any) => {
|
|
172
|
+
const wrapNodesInSystemGroup = (systemNodes: any[], system: any, memberIds?: Set<string>) => {
|
|
136
173
|
const groupId = `system-group-${system.data.id}-${system.data.version}`;
|
|
137
174
|
|
|
138
175
|
const groupNode = {
|
|
@@ -149,5 +186,11 @@ const wrapNodesInSystemGroup = (systemNodes: any[], system: any) => {
|
|
|
149
186
|
};
|
|
150
187
|
|
|
151
188
|
// Parent must come before its children in the array for React Flow
|
|
152
|
-
return [
|
|
189
|
+
return [
|
|
190
|
+
groupNode,
|
|
191
|
+
...systemNodes.map((node) => {
|
|
192
|
+
if (memberIds && SERVICE_NODE_TYPES.has(node.type) && !memberIds.has(node.id)) return withoutGroupParent(node);
|
|
193
|
+
return { ...node, parentId: groupId, extent: 'parent' };
|
|
194
|
+
}),
|
|
195
|
+
];
|
|
153
196
|
};
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "BUSL-1.1",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.5.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -141,11 +141,11 @@
|
|
|
141
141
|
"y-protocols": "^1.0.7",
|
|
142
142
|
"yjs": "^13.6.33",
|
|
143
143
|
"zod": "^4.6.6",
|
|
144
|
+
"@eventcatalog/exporters": "0.1.2",
|
|
144
145
|
"@eventcatalog/fitness": "0.1.0",
|
|
145
|
-
"@eventcatalog/visualiser": "^5.0.0",
|
|
146
146
|
"@eventcatalog/linter": "1.2.4",
|
|
147
|
-
"@eventcatalog/
|
|
148
|
-
"@eventcatalog/
|
|
147
|
+
"@eventcatalog/visualiser": "^5.1.0",
|
|
148
|
+
"@eventcatalog/sdk": "2.31.2"
|
|
149
149
|
},
|
|
150
150
|
"devDependencies": {
|
|
151
151
|
"@astrojs/check": "^0.9.10",
|
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import type { Node, Edge } from '@xyflow/react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mermaid Export Utility
|
|
5
|
-
* Converts React Flow nodes and edges to Mermaid flowchart syntax
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface MermaidExportOptions {
|
|
9
|
-
/** Include class definitions for styling */
|
|
10
|
-
includeStyles?: boolean;
|
|
11
|
-
/** Direction of the flowchart: LR (left-right), TB (top-bottom), etc. */
|
|
12
|
-
direction?: 'LR' | 'TB' | 'RL' | 'BT';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Mapping of node types to their Mermaid shape syntax
|
|
17
|
-
* Format: [prefix, suffix]
|
|
18
|
-
*/
|
|
19
|
-
const NODE_SHAPE_MAP: Record<string, [string, string]> = {
|
|
20
|
-
agents: ['[[', ']]'],
|
|
21
|
-
agent: ['[[', ']]'],
|
|
22
|
-
agentTool: ['[', ']'],
|
|
23
|
-
'agent-tool': ['[', ']'],
|
|
24
|
-
services: ['[[', ']]'], // stadium shape
|
|
25
|
-
service: ['[[', ']]'],
|
|
26
|
-
events: ['>', ']'], // flag/asymmetric shape (message-like)
|
|
27
|
-
event: ['>', ']'],
|
|
28
|
-
commands: ['>', ']'], // flag/asymmetric shape (message-like)
|
|
29
|
-
command: ['>', ']'],
|
|
30
|
-
queries: ['{{', '}}'], // hexagon
|
|
31
|
-
query: ['{{', '}}'],
|
|
32
|
-
channels: ['[(', ')]'], // cylinder
|
|
33
|
-
channel: ['[(', ')]'],
|
|
34
|
-
domains: ['[', ']'], // rectangle
|
|
35
|
-
domain: ['[', ']'],
|
|
36
|
-
'context-domain': ['[', ']'], // another domain, shown as a single card
|
|
37
|
-
systems: ['[[', ']]'], // a system shown as a single node
|
|
38
|
-
'context-actor': ['((', '))'], // circle
|
|
39
|
-
flows: ['([', '])'], // stadium (rounded)
|
|
40
|
-
flow: ['([', '])'],
|
|
41
|
-
system: ['[[', ']]'],
|
|
42
|
-
step: ['[', ']'], // rectangle
|
|
43
|
-
user: ['((', '))'], // circle
|
|
44
|
-
actor: ['((', '))'], // circle
|
|
45
|
-
externalSystem: ['[[', ']]'], // stadium
|
|
46
|
-
'external-system': ['[[', ']]'],
|
|
47
|
-
data: ['[(', ')]'], // cylinder (database)
|
|
48
|
-
'data-product': ['[[', ']]'], // stadium
|
|
49
|
-
'data-products': ['[[', ']]'],
|
|
50
|
-
entities: ['[', ']'], // rectangle
|
|
51
|
-
entity: ['[', ']'],
|
|
52
|
-
custom: ['[', ']'], // rectangle
|
|
53
|
-
view: ['[', ']'], // rectangle
|
|
54
|
-
note: ['[', ']'], // rectangle
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Node types that are boundaries around other nodes, rendered as subgraphs */
|
|
58
|
-
const GROUP_NODE_TYPES = new Set(['system-group', 'domain-group']);
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Mermaid class definitions for styling different node types
|
|
62
|
-
*/
|
|
63
|
-
const NODE_STYLE_CLASSES: Record<string, string> = {
|
|
64
|
-
agents: 'fill:#0ea5e9,stroke:#0369a1,color:#fff',
|
|
65
|
-
agent: 'fill:#0ea5e9,stroke:#0369a1,color:#fff',
|
|
66
|
-
agentTool: 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
67
|
-
'agent-tool': 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
68
|
-
services: 'fill:#ec4899,stroke:#be185d,color:#fff',
|
|
69
|
-
service: 'fill:#ec4899,stroke:#be185d,color:#fff',
|
|
70
|
-
events: 'fill:#f97316,stroke:#c2410c,color:#fff',
|
|
71
|
-
event: 'fill:#f97316,stroke:#c2410c,color:#fff',
|
|
72
|
-
commands: 'fill:#3b82f6,stroke:#1d4ed8,color:#fff',
|
|
73
|
-
command: 'fill:#3b82f6,stroke:#1d4ed8,color:#fff',
|
|
74
|
-
queries: 'fill:#22c55e,stroke:#15803d,color:#fff',
|
|
75
|
-
query: 'fill:#22c55e,stroke:#15803d,color:#fff',
|
|
76
|
-
channels: 'fill:#6b7280,stroke:#374151,color:#fff',
|
|
77
|
-
channel: 'fill:#6b7280,stroke:#374151,color:#fff',
|
|
78
|
-
domains: 'fill:#eab308,stroke:#a16207,color:#000',
|
|
79
|
-
domain: 'fill:#eab308,stroke:#a16207,color:#000',
|
|
80
|
-
'context-domain': 'fill:#eab308,stroke:#a16207,color:#000',
|
|
81
|
-
'context-actor': 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
82
|
-
flows: 'fill:#14b8a6,stroke:#0f766e,color:#fff',
|
|
83
|
-
flow: 'fill:#14b8a6,stroke:#0f766e,color:#fff',
|
|
84
|
-
systems: 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
85
|
-
system: 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
86
|
-
step: 'fill:#374151,stroke:#1f2937,color:#fff',
|
|
87
|
-
user: 'fill:#8b5cf6,stroke:#6d28d9,color:#fff',
|
|
88
|
-
actor: 'fill:#eab308,stroke:#a16207,color:#000',
|
|
89
|
-
externalSystem: 'fill:#ec4899,stroke:#be185d,color:#fff',
|
|
90
|
-
'external-system': 'fill:#ec4899,stroke:#be185d,color:#fff',
|
|
91
|
-
data: 'fill:#3b82f6,stroke:#1d4ed8,color:#fff',
|
|
92
|
-
'data-product': 'fill:#6366f1,stroke:#4338ca,color:#fff',
|
|
93
|
-
'data-products': 'fill:#6366f1,stroke:#4338ca,color:#fff',
|
|
94
|
-
entities: 'fill:#6b7280,stroke:#374151,color:#fff',
|
|
95
|
-
entity: 'fill:#6b7280,stroke:#374151,color:#fff',
|
|
96
|
-
custom: 'fill:#9ca3af,stroke:#6b7280,color:#000',
|
|
97
|
-
view: 'fill:#9ca3af,stroke:#6b7280,color:#000',
|
|
98
|
-
note: 'fill:#fef3c7,stroke:#d97706,color:#000',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Sanitize a string to be a valid Mermaid node ID
|
|
103
|
-
* Mermaid IDs should be alphanumeric with underscores
|
|
104
|
-
*/
|
|
105
|
-
export function sanitizeMermaidId(id: string): string {
|
|
106
|
-
// Replace any non-alphanumeric characters (except underscores) with underscores
|
|
107
|
-
// Also handle dots and hyphens which are common in version strings
|
|
108
|
-
return id.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Escape special characters in Mermaid labels
|
|
113
|
-
*/
|
|
114
|
-
function escapeMermaidLabel(label: string): string {
|
|
115
|
-
// Escape quotes and other special characters
|
|
116
|
-
return label.replace(/"/g, '#quot;').replace(/\n/g, '<br/>');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get the Mermaid shape syntax for a node type
|
|
121
|
-
*/
|
|
122
|
-
export function getMermaidNodeShape(type: string): [string, string] {
|
|
123
|
-
return NODE_SHAPE_MAP[type] || ['[', ']'];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Helper to format label with version
|
|
128
|
-
*/
|
|
129
|
-
function formatLabelWithVersion(name: string, version?: string): string {
|
|
130
|
-
if (version) {
|
|
131
|
-
return `${name} (${version})`;
|
|
132
|
-
}
|
|
133
|
-
return name;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Extract the display label from a node based on its type and data
|
|
138
|
-
* Includes version information when available
|
|
139
|
-
*/
|
|
140
|
-
export function getNodeLabel(node: Node): string {
|
|
141
|
-
const { type, data } = node;
|
|
142
|
-
|
|
143
|
-
if (!data) return node.id;
|
|
144
|
-
|
|
145
|
-
// Handle different node types and their data structures
|
|
146
|
-
if (type === 'agents' || type === 'agent') {
|
|
147
|
-
const agent = (data as any).agent;
|
|
148
|
-
const name = agent?.name || agent?.id || node.id;
|
|
149
|
-
const version = agent?.data?.version || agent?.version;
|
|
150
|
-
return formatLabelWithVersion(name, version);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (type === 'agentTool' || type === 'agent-tool') {
|
|
154
|
-
const agentTool = (data as any).agentTool;
|
|
155
|
-
return agentTool?.name || agentTool?.id || node.id;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (type === 'services' || type === 'service') {
|
|
159
|
-
const service = (data as any).service;
|
|
160
|
-
const name = service?.name || service?.id || node.id;
|
|
161
|
-
const version = service?.data?.version || service?.version;
|
|
162
|
-
return formatLabelWithVersion(name, version);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
type === 'events' ||
|
|
167
|
-
type === 'event' ||
|
|
168
|
-
type === 'commands' ||
|
|
169
|
-
type === 'command' ||
|
|
170
|
-
type === 'queries' ||
|
|
171
|
-
type === 'query'
|
|
172
|
-
) {
|
|
173
|
-
const message = (data as any).message;
|
|
174
|
-
const name = message?.name || message?.id || node.id;
|
|
175
|
-
const version = message?.data?.version || message?.version;
|
|
176
|
-
return formatLabelWithVersion(name, version);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (type === 'channels' || type === 'channel') {
|
|
180
|
-
const channel = (data as any).channel;
|
|
181
|
-
const name = channel?.name || channel?.id || node.id;
|
|
182
|
-
const version = channel?.data?.version || channel?.version;
|
|
183
|
-
return formatLabelWithVersion(name, version);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (type === 'systems' || type === 'system-group') {
|
|
187
|
-
const system = (data as any).system;
|
|
188
|
-
return formatLabelWithVersion(system?.name || system?.id || node.id, system?.version);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (type === 'context-domain' || type === 'domain-group') {
|
|
192
|
-
const domain = (data as any).domain;
|
|
193
|
-
return formatLabelWithVersion(domain?.name || domain?.id || node.id, domain?.version);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (type === 'domains' || type === 'domain') {
|
|
197
|
-
const domain = (data as any).domain;
|
|
198
|
-
// Domain data can be nested in .data
|
|
199
|
-
const domainData = domain?.data || domain;
|
|
200
|
-
const name = domainData?.name || domainData?.id || node.id;
|
|
201
|
-
const version = domainData?.version || domain?.version;
|
|
202
|
-
return formatLabelWithVersion(name, version);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (type === 'systems' || type === 'system') {
|
|
206
|
-
const system = (data as any).system;
|
|
207
|
-
const name = system?.name || system?.id || node.id;
|
|
208
|
-
const version = system?.data?.version || system?.version;
|
|
209
|
-
return formatLabelWithVersion(name, version);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (type === 'flows' || type === 'flow') {
|
|
213
|
-
const flow = (data as any).flow;
|
|
214
|
-
const name = flow?.name || flow?.id || node.id;
|
|
215
|
-
const version = flow?.data?.version || flow?.version;
|
|
216
|
-
return formatLabelWithVersion(name, version);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (type === 'step') {
|
|
220
|
-
const step = (data as any).step;
|
|
221
|
-
return step?.title || step?.name || step?.id || node.id;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (type === 'user' || type === 'actor') {
|
|
225
|
-
return (data as any).name || (data as any).label || (data as any).id || node.id;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (type === 'externalSystem' || type === 'external-system') {
|
|
229
|
-
const system = (data as any).externalSystem || data;
|
|
230
|
-
return system?.name || system?.id || node.id;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (type === 'data') {
|
|
234
|
-
const dataNode = (data as any).data;
|
|
235
|
-
return dataNode?.name || dataNode?.id || node.id;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (type === 'data-product' || type === 'data-products') {
|
|
239
|
-
const dataProduct = (data as any).dataProduct;
|
|
240
|
-
const name = dataProduct?.name || dataProduct?.id || node.id;
|
|
241
|
-
const version = dataProduct?.data?.version || dataProduct?.version;
|
|
242
|
-
return formatLabelWithVersion(name, version);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (type === 'entities' || type === 'entity') {
|
|
246
|
-
const entity = (data as any).entity;
|
|
247
|
-
return entity?.name || entity?.id || node.id;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (type === 'custom') {
|
|
251
|
-
const custom = (data as any).custom ?? {};
|
|
252
|
-
return custom.title || (data as any).step?.title || node.id;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (type === 'note') {
|
|
256
|
-
return (data as any).text || (data as any).label || 'Note';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Fallback: try common data patterns
|
|
260
|
-
return (data as any).name || (data as any).label || (data as any).title || (data as any).id || node.id;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Extract label from an edge
|
|
265
|
-
*/
|
|
266
|
-
function getEdgeLabel(edge: Edge): string | undefined {
|
|
267
|
-
if (edge.label && typeof edge.label === 'string') {
|
|
268
|
-
return edge.label;
|
|
269
|
-
}
|
|
270
|
-
if (edge.data?.label && typeof edge.data.label === 'string') {
|
|
271
|
-
return edge.data.label;
|
|
272
|
-
}
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Convert React Flow nodes and edges to Mermaid flowchart syntax
|
|
278
|
-
*/
|
|
279
|
-
export function convertToMermaid(nodes: Node[], edges: Edge[], options: MermaidExportOptions = {}): string {
|
|
280
|
-
const { includeStyles = true, direction = 'LR' } = options;
|
|
281
|
-
|
|
282
|
-
const lines: string[] = [];
|
|
283
|
-
|
|
284
|
-
// Add flowchart header
|
|
285
|
-
lines.push(`flowchart ${direction}`);
|
|
286
|
-
|
|
287
|
-
// Collect used node types for class definitions
|
|
288
|
-
const usedTypes = new Set<string>();
|
|
289
|
-
|
|
290
|
-
// Add class definitions if styles are enabled
|
|
291
|
-
if (includeStyles) {
|
|
292
|
-
lines.push('');
|
|
293
|
-
lines.push(' %% Style definitions');
|
|
294
|
-
|
|
295
|
-
// First pass: collect all used node types
|
|
296
|
-
nodes.forEach((node) => {
|
|
297
|
-
if (node.type && NODE_STYLE_CLASSES[node.type]) {
|
|
298
|
-
usedTypes.add(node.type);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Add class definitions for used types
|
|
303
|
-
usedTypes.forEach((type) => {
|
|
304
|
-
const style = NODE_STYLE_CLASSES[type];
|
|
305
|
-
if (style) {
|
|
306
|
-
lines.push(` classDef ${sanitizeMermaidId(type)} ${style}`);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Add node definitions. Nodes inside a group (a system or domain boundary)
|
|
312
|
-
// are nested in a subgraph for that group, so containment is kept
|
|
313
|
-
lines.push('');
|
|
314
|
-
lines.push(' %% Nodes');
|
|
315
|
-
|
|
316
|
-
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
317
|
-
const childrenOf = new Map<string, Node[]>();
|
|
318
|
-
const topLevelNodes: Node[] = [];
|
|
319
|
-
nodes.forEach((node) => {
|
|
320
|
-
if (node.parentId && nodeIds.has(node.parentId)) {
|
|
321
|
-
childrenOf.set(node.parentId, [...(childrenOf.get(node.parentId) ?? []), node]);
|
|
322
|
-
} else {
|
|
323
|
-
topLevelNodes.push(node);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
const addNode = (node: Node, depth: number) => {
|
|
328
|
-
const indent = ' '.repeat(depth);
|
|
329
|
-
const sanitizedId = sanitizeMermaidId(node.id);
|
|
330
|
-
const label = escapeMermaidLabel(getNodeLabel(node));
|
|
331
|
-
const children = childrenOf.get(node.id);
|
|
332
|
-
|
|
333
|
-
if (children || GROUP_NODE_TYPES.has(node.type ?? '')) {
|
|
334
|
-
lines.push(`${indent}subgraph ${sanitizedId}["${label}"]`);
|
|
335
|
-
children?.forEach((child) => addNode(child, depth + 1));
|
|
336
|
-
lines.push(`${indent}end`);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const [prefix, suffix] = getMermaidNodeShape(node.type || 'custom');
|
|
341
|
-
let nodeLine = `${indent}${sanitizedId}${prefix}"${label}"${suffix}`;
|
|
342
|
-
|
|
343
|
-
// Add class reference if styles are enabled
|
|
344
|
-
if (includeStyles && node.type && usedTypes.has(node.type)) {
|
|
345
|
-
nodeLine += `:::${sanitizeMermaidId(node.type)}`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
lines.push(nodeLine);
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
topLevelNodes.forEach((node) => addNode(node, 1));
|
|
352
|
-
|
|
353
|
-
// Add edge definitions
|
|
354
|
-
lines.push('');
|
|
355
|
-
lines.push(' %% Edges');
|
|
356
|
-
|
|
357
|
-
edges.forEach((edge) => {
|
|
358
|
-
const sourceId = sanitizeMermaidId(edge.source);
|
|
359
|
-
const targetId = sanitizeMermaidId(edge.target);
|
|
360
|
-
const label = getEdgeLabel(edge);
|
|
361
|
-
|
|
362
|
-
let edgeLine: string;
|
|
363
|
-
if (label) {
|
|
364
|
-
// Clean up multi-line labels
|
|
365
|
-
const cleanLabel = label.replace(/\n/g, ' ').trim();
|
|
366
|
-
edgeLine = ` ${sourceId} -->|"${escapeMermaidLabel(cleanLabel)}"| ${targetId}`;
|
|
367
|
-
} else {
|
|
368
|
-
edgeLine = ` ${sourceId} --> ${targetId}`;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
lines.push(edgeLine);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
return lines.join('\n');
|
|
375
|
-
}
|