|
| 1 | +import { FabloConfigExtended, OrdererGroup } from "../../types/FabloConfigExtended"; |
| 2 | + |
| 3 | +const safeId = (id: string): string => id.replace(/[^a-zA-Z0-9_]/g, "_"); |
| 4 | +const ordererGroupId = (g: OrdererGroup): string => safeId(`ord_group_${g.name}_${g.orderers?.[0].address}`); |
| 5 | +const channelId = (channelName: string): string => safeId(`channel_${channelName}`); |
| 6 | +const chaincodeId = (ccName: string): string => safeId(`chaincode_${ccName}`); |
| 7 | + |
| 8 | +export function generateMermaidDiagram(config: FabloConfigExtended): string { |
| 9 | + const lines: string[] = ["graph LR"]; |
| 10 | + lines.push(""); |
| 11 | + lines.push("classDef subgraph_padding fill:none,stroke:none"); |
| 12 | + |
| 13 | + // Add organization subgraphs with orderer groups, CA, and peers |
| 14 | + config.orgs?.forEach((org) => { |
| 15 | + const orgId = safeId(org.domain); |
| 16 | + lines.push(`\n subgraph ${orgId} [Organization: ${org.name}<br>${org.domain}]`); |
| 17 | + const orgPaddingId = `${orgId}_padding`; |
| 18 | + lines.push(` subgraph ${orgPaddingId} [ ]`); |
| 19 | + lines.push(" direction RL"); |
| 20 | + |
| 21 | + // Orderer groups (nested inside org) |
| 22 | + org.ordererGroups?.forEach((group) => { |
| 23 | + if (group.orderers && group.orderers.length > 0) { |
| 24 | + const consensusLabel = group.consensus ? group.consensus : ""; |
| 25 | + const groupId = ordererGroupId(group); |
| 26 | + lines.push(` subgraph ${groupId} [Orderer Group: ${group.name}<br>${consensusLabel}]`); |
| 27 | + const groupPaddingId = `${groupId}_padding`; |
| 28 | + lines.push(` subgraph ${groupPaddingId} [ ]`); |
| 29 | + lines.push(" direction RL"); |
| 30 | + group.orderers.forEach((orderer) => { |
| 31 | + lines.push(` ${safeId(orderer.address)}[${orderer.address}]`); |
| 32 | + }); |
| 33 | + lines.push(` end`); |
| 34 | + lines.push(` class ${groupPaddingId} subgraph_padding`); |
| 35 | + lines.push(" end"); |
| 36 | + } |
| 37 | + }); |
| 38 | + |
| 39 | + // CA (at same level as orderer groups) |
| 40 | + if (org.ca) { |
| 41 | + const caAddress = org.ca.address; |
| 42 | + const caLabel = org.ca.db ? `${caAddress}<br>${org.ca.db}` : `${caAddress}`; |
| 43 | + lines.push(` ${safeId(caAddress)}([${caLabel}])`); |
| 44 | + } |
| 45 | + |
| 46 | + // Peers (at same level as orderer groups) |
| 47 | + org.peers?.forEach((peer) => { |
| 48 | + const peerLabel = `${peer.address}<br>${peer.db.type}`; |
| 49 | + lines.push(` ${safeId(peer.address)}[${peerLabel}]`); |
| 50 | + }); |
| 51 | + |
| 52 | + lines.push(" end"); |
| 53 | + lines.push(` class ${orgPaddingId} subgraph_padding`); |
| 54 | + lines.push(" end"); |
| 55 | + }); |
| 56 | + |
| 57 | + // Add channel subgraphs with chaincodes |
| 58 | + config.channels?.forEach((channel) => { |
| 59 | + const chId = channelId(channel.name); |
| 60 | + lines.push(`\n subgraph ${chId} [Channel: ${channel.name}]`); |
| 61 | + const chPaddingId = `${chId}_padding`; |
| 62 | + lines.push(` subgraph ${chPaddingId} [ ]`); |
| 63 | + |
| 64 | + // Add chaincodes for this channel (using cylinder shape) |
| 65 | + const channelChaincodes = config.chaincodes?.filter((cc) => cc.channel?.name === channel.name) ?? []; |
| 66 | + channelChaincodes.forEach((cc) => { |
| 67 | + lines.push(` ${chaincodeId(cc.name)}[[Chaincode: ${cc.name}]]`); |
| 68 | + }); |
| 69 | + |
| 70 | + // Add dummy invisible node for empty channels to ensure visibility |
| 71 | + if (channelChaincodes.length === 0) { |
| 72 | + const emptyNodeId = `${chId}_empty`; |
| 73 | + lines.push(` ${emptyNodeId}[" "]`); |
| 74 | + lines.push(` style ${emptyNodeId} fill:#ffffff00,stroke:#ffffff00`); |
| 75 | + } |
| 76 | + |
| 77 | + lines.push(" end"); |
| 78 | + lines.push(` class ${chPaddingId} subgraph_padding`); |
| 79 | + lines.push(" end"); |
| 80 | + }); |
| 81 | + |
| 82 | + // Add connections |
| 83 | + lines.push("\n %% Connections"); |
| 84 | + |
| 85 | + // Connect peers to channels |
| 86 | + config.channels?.forEach((channel) => { |
| 87 | + const channelIdStr = channelId(channel.name); |
| 88 | + |
| 89 | + channel.orgs?.forEach((orgOnChannel) => { |
| 90 | + orgOnChannel.peers?.forEach((peer) => { |
| 91 | + lines.push(` ${safeId(peer.address)} --> ${channelIdStr}`); |
| 92 | + }); |
| 93 | + }); |
| 94 | + }); |
| 95 | + |
| 96 | + // Connect channels to orderer groups (reversed direction) |
| 97 | + config.channels?.forEach((channel) => { |
| 98 | + const channelIdStr = channelId(channel.name); |
| 99 | + const ogId = ordererGroupId(channel.ordererGroup); |
| 100 | + lines.push(` ${channelIdStr} --> ${ogId}`); |
| 101 | + }); |
| 102 | + |
| 103 | + return lines.join("\n"); |
| 104 | +} |
0 commit comments