import { writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Graphviz } from '@hpcc-js/wasm-graphviz'; import { getOpenApiSpec } from './src/api/openapiSpec.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const outputPath = join(__dirname, 'api-graph.svg'); const dotOutputPath = join(__dirname, 'api-graph.dot'); const COLORS = { background: '#1e2430', tableBorder: '#3f4c5f', headerBg: '#33415c', headerText: '#e2e8f0', rowBg: '#2a3242', rowBgAlt: '#272e3d', keyBg: '#2b3a52', text: '#d8dee9', muted: '#8b95a7', clusterFill: '#202a3a', }; const METHOD_COLORS = { GET: { fg: '#38bdf8', bg: '#182b40' }, POST: { fg: '#4ade80', bg: '#143826' }, PUT: { fg: '#fbbf24', bg: '#3b2c12' }, PATCH: { fg: '#c084fc', bg: '#301c44' }, DELETE: { fg: '#f87171', bg: '#3a181e' }, }; const EDGE_COLORS = [ '#ff6b6b', '#6bdf9a', '#5aa7ff', '#f5c15c', '#c79bff', '#ff9f5a', '#5cd6d6', '#ff7fc2', '#8f95ff', '#58d18a', '#ff8f66', '#4fc4e8', '#a98cff', '#ff6f6f', '#4ecb8f', '#5aa7e6', '#ff7aa8', '#e0b25a', '#56c8c8', '#ff5f9e', '#ffb25a', '#56d68a', '#66b3ff', '#c79bff', ]; const CLUSTERS = [ { id: 'identity', label: 'Identity & Access', resources: ['Auth', 'Company', 'CompanySetting', 'Customergroup', 'Number'], }, { id: 'crm', label: 'Entities & CRM', resources: ['Entity'], }, { id: 'catalog', label: 'Catalog & Products', resources: ['Article', 'Category', 'Brand', 'Unit', 'PublicCatalog'], }, { id: 'properties', label: 'Properties & Attributes', resources: ['Property', 'Attribute'], }, { id: 'purchasing', label: 'Suppliers & Purchasing', resources: ['Supplier', 'ArticleSupplier', 'Delivery'], }, { id: 'sales', label: 'Sales & Fulfillment', resources: ['Quote', 'Order', 'Shipment', 'Shipping', 'Package'], }, { id: 'inventory', label: 'Inventory & Warehousing', resources: ['Stock', 'Ledger', 'Warehouse'], }, { id: 'finance', label: 'Finance & Payments', resources: ['Account', 'Payment'], }, { id: 'media', label: 'Media & Storage', resources: ['Image', 'Serper', 'Vfs'], }, { id: 'pos', label: 'POS & Hardware', resources: ['POSTerminal', 'Printer', 'LabelTemplate', 'POSSync'], }, { id: 'ai_mcp', label: 'AI & Integrations', resources: ['ModelConversation', 'MCP'], }, { id: 'system', label: 'System & Realtime', resources: ['Events', 'Health', 'Telemetry', 'Legend'], }, ]; function escapeHtml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** * Maps an OpenAPI endpoint to its logical API resource card. */ function assignResource(apiPath, method, tag) { if (apiPath.startsWith('/auth') || apiPath.startsWith('/.well-known')) return 'Auth'; if (apiPath.startsWith('/company/setting') || apiPath.startsWith('/companysetting')) return 'CompanySetting'; if (apiPath.startsWith('/company')) return 'Company'; if (apiPath.startsWith('/customergroup')) return 'Customergroup'; if (apiPath.startsWith('/number')) return 'Number'; if (apiPath.startsWith('/entity')) return 'Entity'; if (apiPath.startsWith('/propertyname')) return 'Property'; if (apiPath.startsWith('/attributename') || apiPath.startsWith('/attributevalue')) return 'Attribute'; if (apiPath.startsWith('/article/') && apiPath.includes('/supplier')) return 'ArticleSupplier'; if (apiPath.startsWith('/article/') && apiPath.includes('/property')) return 'Property'; if (apiPath.startsWith('/article/') && apiPath.includes('/attributevalue')) return 'Attribute'; if (apiPath.startsWith('/article')) return 'Article'; if (apiPath.startsWith('/category/') && apiPath.includes('/property')) return 'Property'; if (apiPath.startsWith('/category/') && apiPath.includes('/attributevalue')) return 'Attribute'; if (apiPath.startsWith('/category')) return 'Category'; if (apiPath.startsWith('/brand')) return 'Brand'; if (apiPath.startsWith('/unit')) return 'Unit'; if (apiPath.startsWith('/public')) return 'PublicCatalog'; if (apiPath.startsWith('/supplier')) return 'Supplier'; if (apiPath.startsWith('/delivery')) return 'Delivery'; if (apiPath.startsWith('/quote')) return 'Quote'; if (apiPath.startsWith('/order')) return 'Order'; if (apiPath.startsWith('/shipment')) return 'Shipment'; if (apiPath.startsWith('/shipping')) return 'Shipping'; if (apiPath.startsWith('/package')) return 'Package'; if (apiPath.startsWith('/stock')) return 'Stock'; if (apiPath.startsWith('/ledger')) return 'Ledger'; if (apiPath.startsWith('/warehouse')) return 'Warehouse'; if (apiPath.startsWith('/account')) return 'Account'; if (apiPath.startsWith('/payment')) return 'Payment'; if (apiPath.startsWith('/image') || apiPath.startsWith('/thumb') || apiPath.startsWith('/pic')) return 'Image'; if (apiPath.startsWith('/serper')) return 'Serper'; if (apiPath.startsWith('/vfs')) return 'Vfs'; if (apiPath.startsWith('/posterminal')) return 'POSTerminal'; if (apiPath.startsWith('/printer')) return 'Printer'; if (apiPath.startsWith('/labeltemplate')) return 'LabelTemplate'; if (apiPath.startsWith('/v1')) return 'POSSync'; if (apiPath.startsWith('/modelconversation')) return 'ModelConversation'; if (apiPath.startsWith('/mcp')) return 'MCP'; if (apiPath.startsWith('/events')) return 'Events'; if (apiPath.startsWith('/health')) return 'Health'; if (apiPath.startsWith('/telemetry')) return 'Telemetry'; return tag || 'Other'; } function getResourceBasePath(resourceId) { const map = { Auth: '/auth', Company: '/company', CompanySetting: '/companysetting', Customergroup: '/customergroup', Number: '/number', Entity: '/entity', Article: '/article', Category: '/category', Brand: '/brand', Unit: '/unit', PublicCatalog: '/public', Property: '/propertyname', Attribute: '/attributename', Supplier: '/supplier', ArticleSupplier: '/article/{sku}/supplier', Delivery: '/delivery', Quote: '/quote', Order: '/order', Shipment: '/shipment', Shipping: '/shipping', Package: '/package', Stock: '/stock', Ledger: '/ledger', Warehouse: '/warehouse', Account: '/account', Payment: '/payment', Image: '/image', Serper: '/serper', Vfs: '/vfs', POSTerminal: '/posterminal', Printer: '/printer', LabelTemplate: '/labeltemplate', POSSync: '/v1', ModelConversation: '/modelconversation', MCP: '/mcp', Events: '/events', Health: '/health', Telemetry: '/telemetry', Legend: '', }; return map[resourceId] ?? `/${resourceId.toLowerCase()}`; } function cleanSubpath(apiPath, basePath) { if (apiPath === basePath) return '/'; if (apiPath.startsWith(basePath)) { const sub = apiPath.slice(basePath.length); return sub.startsWith('/') ? sub : `/${sub}`; } return apiPath; } function truncate(str, max = 28) { if (!str) return ''; return str.length > max ? `${str.slice(0, max - 1)}…` : str; } function sanitizePort(name) { return name.replace(/[^a-zA-Z0-9_]/g, '_'); } function parseApi(spec) { const resources = new Map(); for (const [apiPath, pathItem] of Object.entries(spec.paths || {})) { for (const [method, op] of Object.entries(pathItem || {})) { if (!['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) continue; if (!op || typeof op !== 'object') continue; const methodUpper = method.toUpperCase(); const tag = Array.isArray(op.tags) && op.tags[0] ? op.tags[0] : ''; const resourceId = assignResource(apiPath, methodUpper, tag); const basePath = getResourceBasePath(resourceId); if (!resources.has(resourceId)) { resources.set(resourceId, { id: resourceId, basePath, endpoints: [], }); } const res = resources.get(resourceId); const subpath = cleanSubpath(apiPath, basePath); const summary = op.summary || op.description || ''; const portId = sanitizePort(`ep_${methodUpper.toLowerCase()}_${apiPath}`); res.endpoints.push({ method: methodUpper, path: apiPath, subpath, portId, summary: truncate(summary, 28), operation: op, }); } } // Sort endpoints within each resource: root subpath first, then by method rank, then path const methodRank = { GET: 1, POST: 2, PUT: 3, PATCH: 4, DELETE: 5 }; for (const res of resources.values()) { res.endpoints.sort((a, b) => { if (a.subpath === '/' && b.subpath !== '/') return -1; if (b.subpath === '/' && a.subpath !== '/') return 1; const rankDiff = (methodRank[a.method] ?? 99) - (methodRank[b.method] ?? 99); if (rankDiff !== 0) return rankDiff; return a.subpath.localeCompare(b.subpath); }); } return resources; } function findEndpointPort(res, method, pathSnippet) { if (!res || !res.endpoints) return null; const match = res.endpoints.find((ep) => { if (method && ep.method !== method.toUpperCase()) return false; if (pathSnippet) { return ep.path.includes(pathSnippet) || ep.subpath.includes(pathSnippet); } return true; }); return match?.portId ?? null; } /** * Builds high-fidelity relationships between API resources. */ function buildApiEdges(resources) { const edgeDefs = [ // Sales & Fulfillment { from: 'Order', to: 'Entity', label: 'entityId', method: 'GET', path: '/order/{id}' }, { from: 'Order', to: 'Article', label: 'lines.sku', method: 'POST', path: '/order' }, { from: 'Order', to: 'Shipment', label: 'shipments', method: 'GET', path: '/order/{id}' }, { from: 'Order', to: 'Payment', label: 'payments', method: 'GET', path: '/order/{id}' }, { from: 'Order', to: 'Shipping', label: 'shippings', method: 'GET', path: '/order/{id}' }, { from: 'Quote', to: 'Entity', label: 'entityId', method: 'GET', path: '/quote/{id}' }, { from: 'Quote', to: 'Article', label: 'lines.sku', method: 'POST', path: '/quote' }, { from: 'Quote', to: 'Order', label: 'convert order', method: 'POST', path: '/quote' }, { from: 'Shipment', to: 'Order', label: 'manifestId', method: 'GET', path: '/shipment/{id}' }, { from: 'Shipment', to: 'Article', label: 'lines.sku', method: 'GET', path: '/shipment/{id}' }, { from: 'Shipment', to: 'Shipping', label: 'shippings', method: 'GET', path: '/shipment/{id}/shipping' }, { from: 'Shipping', to: 'Package', label: 'packages', method: 'GET', path: '/shipping/{id}' }, { from: 'Package', to: 'Shipping', label: 'shippingId', method: 'POST', path: '/package' }, { from: 'Package', to: 'Entity', label: 'addressId', method: 'POST', path: '/package' }, // Purchasing & Goods receipt { from: 'Delivery', to: 'Supplier', label: 'supplierId', method: 'POST', path: '/delivery' }, { from: 'Delivery', to: 'Article', label: 'lines.sku', method: 'POST', path: '/delivery' }, { from: 'Delivery', to: 'Ledger', label: 'ledger', method: 'GET', path: '/delivery/{id}' }, { from: 'ArticleSupplier', to: 'Supplier', label: 'entityId', method: 'POST', path: '/supplier' }, { from: 'ArticleSupplier', to: 'Article', label: 'sku', method: 'GET', path: '/supplier' }, { from: 'Supplier', to: 'Entity', label: 'entityId', method: 'GET', path: '/supplier/{id}' }, // Inventory & Warehousing { from: 'Stock', to: 'Article', label: 'sku', method: 'POST', path: '/stock' }, { from: 'Stock', to: 'Warehouse', label: 'warehouseId', method: 'POST', path: '/stock' }, { from: 'Stock', to: 'Ledger', label: 'ledger', method: 'POST', path: '/stock' }, { from: 'Ledger', to: 'Article', label: 'sku', method: 'GET', path: '/ledger' }, { from: 'Ledger', to: 'Warehouse', label: 'warehouseId', method: 'GET', path: '/ledger' }, // Catalog & Products { from: 'Article', to: 'Category', label: 'categoryId', method: 'POST', path: '/article' }, { from: 'Article', to: 'Brand', label: 'brand', method: 'PUT', path: '/brand' }, { from: 'Article', to: 'Unit', label: 'unitSi', method: 'PATCH', path: '/article/{sku}' }, { from: 'Article', to: 'ArticleSupplier', label: 'suppliers', method: 'GET', path: '/article/{sku}' }, { from: 'Article', to: 'Image', label: 'photos', method: 'POST', path: '/image' }, { from: 'Article', to: 'Customergroup', label: 'prices', method: 'PUT', path: '/prices' }, { from: 'Article', to: 'Property', label: 'properties', method: 'GET', path: '/property' }, { from: 'Article', to: 'Attribute', label: 'attributes', method: 'GET', path: '/attributevalue' }, { from: 'Article', to: 'Article', label: 'bundles & variants', method: 'GET', path: '/children' }, { from: 'Category', to: 'Category', label: 'parentId', method: 'GET', path: '/category' }, { from: 'Category', to: 'Article', label: 'articles', method: 'GET', path: '/category/{id}/article' }, { from: 'Category', to: 'Image', label: 'image', method: 'POST', path: '/image' }, { from: 'Category', to: 'Property', label: 'properties', method: 'GET', path: '/property' }, { from: 'Category', to: 'Attribute', label: 'attributes', method: 'GET', path: '/attributevalue' }, { from: 'Brand', to: 'Image', label: 'image', method: 'POST', path: '/image' }, // Properties & Attributes { from: 'Property', to: 'Company', label: 'companyId', method: 'GET', path: '/propertyname' }, { from: 'Attribute', to: 'Image', label: 'image', method: 'POST', path: '/image' }, // Finance & Payments { from: 'Payment', to: 'Account', label: 'accountId', method: 'POST', path: '/payment' }, { from: 'Payment', to: 'Order', label: 'manifestId', method: 'POST', path: '/payment' }, { from: 'Account', to: 'Company', label: 'companyId', method: 'GET', path: '/account' }, // CRM & Company { from: 'Entity', to: 'Customergroup', label: 'customergroupId', method: 'POST', path: '/entity' }, { from: 'Company', to: 'Customergroup', label: 'poscustomergroup', method: 'GET', path: '/company/{id}' }, { from: 'Company', to: 'Category', label: 'root_category', method: 'GET', path: '/company/{id}' }, { from: 'Company', to: 'CompanySetting', label: 'settings', method: 'GET', path: '/company/{id}' }, { from: 'Number', to: 'Company', label: 'companyId', method: 'GET', path: '/number' }, // POS & Hardware { from: 'POSTerminal', to: 'Company', label: 'companyId', method: 'GET', path: '/posterminal' }, { from: 'POSTerminal', to: 'Category', label: 'root_category', method: 'GET', path: '/posterminal' }, { from: 'Printer', to: 'Company', label: 'companyId', method: 'GET', path: '/printer' }, { from: 'Printer', to: 'LabelTemplate', label: 'templates', method: 'GET', path: '/printer' }, { from: 'LabelTemplate', to: 'Company', label: 'companyId', method: 'GET', path: '/labeltemplate' }, { from: 'POSSync', to: 'POSTerminal', label: 'client bind', method: 'POST', path: '/client' }, { from: 'POSSync', to: 'Article', label: 'product sync', method: 'GET', path: '/product' }, { from: 'POSSync', to: 'Category', label: 'category sync', method: 'GET', path: '/category' }, // AI, Tools & Public { from: 'PublicCatalog', to: 'Article', label: 'articles', method: 'GET', path: '/product' }, { from: 'PublicCatalog', to: 'Category', label: 'category', method: 'GET', path: '/category' }, { from: 'ModelConversation', to: 'Vfs', label: 'vfs workspace', method: 'GET', path: '/modelconversation' }, { from: 'MCP', to: 'Article', label: 'article tools', method: 'POST', path: '/mcp' }, { from: 'MCP', to: 'Order', label: 'order tools', method: 'POST', path: '/mcp' }, { from: 'Events', to: 'Stock', label: 'stock sse', method: 'GET', path: '/events' }, { from: 'Events', to: 'Ledger', label: 'ledger sse', method: 'GET', path: '/events' }, { from: 'Events', to: 'Article', label: 'article sse', method: 'GET', path: '/events' }, ]; const edges = []; for (const def of edgeDefs) { if (!resources.has(def.from) || !resources.has(def.to)) continue; const resFrom = resources.get(def.from); const resolvedPort = findEndpointPort(resFrom, def.method, def.path); edges.push({ from: def.from, to: def.to, label: def.label, port: resolvedPort, tooltip: `${def.from} → ${def.to} (${def.label})`, inferred: false, }); } return edges; } function edgeKey(edge) { return `${edge.from}|${edge.to}|${edge.label}`; } function assignEdgeColors(edges) { const colors = new Map(); let next = 0; for (const edge of edges) { const key = edgeKey(edge); if (!colors.has(key)) { colors.set(key, EDGE_COLORS[next % EDGE_COLORS.length]); next++; } } return colors; } /** * Spanning ranking forest: only one primary outgoing edge per resource * is used for rank constraints. Other edges get constraint=false to avoid * vertical over-stretching. */ function selectRankingEdgeKeys(edges) { const ranking = new Set(); const bySource = new Map(); for (const edge of edges) { if (edge.from === edge.to) continue; if (!bySource.has(edge.from)) bySource.set(edge.from, []); bySource.get(edge.from).push(edge); } for (const group of bySource.values()) { group.sort((a, b) => a.label.localeCompare(b.label)); ranking.add(edgeKey(group[0])); } return ranking; } function buildLegendLabel() { const methodRow = (method, desc) => { const c = METHOD_COLORS[method]; return ( `` + `${method}` + `${desc}` ); }; const row = (swatch, text) => `${swatch}` + `${text}`; return ( `<` + `` + methodRow('GET', 'Safe retrieval / query') + methodRow('POST', 'Create resource / action') + methodRow('PUT', 'Full replace / collection update') + methodRow('PATCH', 'Partial entity update') + methodRow('DELETE', 'Remove resource') + row( `in#`, 'Inbound arrival port on resource' ) + row( `▪—▶`, 'Relational reference / dataflow' ) + `
` + `API Graph Legend
>` ); } function renderResourceNode(res, inboundCount = 0) { if (res.id === 'Legend') { return buildLegendLabel(); } const titleFont = `${escapeHtml(res.id)}` + `
${escapeHtml(res.basePath)}`; let headerInner; if (inboundCount > 0) { const portCells = []; for (let i = 0; i < inboundCount; i++) { portCells.push( `` ); } headerInner = `` + `${portCells.join('')}` + `` + `
${titleFont}
`; } else { headerInner = titleFont; } const rows = [ `${headerInner}`, ]; for (const [i, ep] of res.endpoints.entries()) { const mColor = METHOD_COLORS[ep.method] ?? { fg: COLORS.text, bg: COLORS.rowBg }; const rowBg = i % 2 === 0 ? COLORS.rowBg : COLORS.rowBgAlt; rows.push( `` + `` + `` + `` + `` + `` + `
` + `${escapeHtml(ep.method)}` + `${escapeHtml(ep.subpath)}` + `${escapeHtml(ep.summary)}
` ); } return ( `<` + rows.join('') + `
>` ); } function assignEdgeAnchors(edges, positions) { const headPort = new Map(); const tailCompass = new Map(); const inboundCounts = new Map(); const byTarget = new Map(); const bySource = new Map(); for (const edge of edges) { if (!byTarget.has(edge.to)) byTarget.set(edge.to, []); byTarget.get(edge.to).push(edge); if (!bySource.has(edge.from)) bySource.set(edge.from, []); bySource.get(edge.from).push(edge); } for (const [target, group] of byTarget) { group.sort((a, b) => { const ax = positions?.get(a.from)?.x ?? 0; const bx = positions?.get(b.from)?.x ?? 0; if (ax !== bx) return ax - bx; return a.label.localeCompare(b.label); }); inboundCounts.set(target, group.length); group.forEach((edge, i) => { headPort.set(edgeKey(edge), `in${i}`); }); } for (const group of bySource.values()) { group.forEach((edge, i) => { if (edge.from === edge.to) { tailCompass.set(edgeKey(edge), i % 2 === 0 ? 'ne' : 'nw'); return; } const from = positions?.get(edge.from); const to = positions?.get(edge.to); const fromX = from?.x ?? 0; const toX = to?.x ?? fromX; const goEast = toX >= fromX; tailCompass.set(edgeKey(edge), goEast ? 'e' : 'w'); }); } return { headPort, tailCompass, inboundCounts }; } function buildDot(resources, edges, positions, anchors) { const edgeColors = assignEdgeColors(edges); const rankingKeys = selectRankingEdgeKeys(edges); const { headPort, tailCompass, inboundCounts } = anchors ?? assignEdgeAnchors(edges, positions); const lines = [ 'digraph api {', ' graph [rankdir=TB, splines=spline, overlap=false, bgcolor="#1e2430",', ' nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica",', ' compound=true, mclimit=1.0];', ' node [shape=plaintext, fontname="Helvetica"];', ' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];', '', ]; const clusteredResources = new Set(); for (const cluster of CLUSTERS) { const presentInCluster = cluster.resources.filter((id) => resources.has(id)); if (presentInCluster.length === 0) continue; lines.push(` subgraph cluster_${cluster.id} {`); lines.push(` label="${escapeHtml(cluster.label)}"; labeljust="l"; fontsize=15;`); lines.push(` style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f";`); lines.push(` fontcolor="#e2e8f0"; margin=14;`); for (const resId of presentInCluster) { clusteredResources.add(resId); const res = resources.get(resId); const inCount = inboundCounts.get(resId) ?? 0; const label = renderResourceNode(res, inCount); lines.push(` ${resId} [label=${label}];`); } lines.push(' }'); } // Any unclustered resources for (const [resId, res] of resources) { if (clusteredResources.has(resId)) continue; const inCount = inboundCounts.get(resId) ?? 0; const label = renderResourceNode(res, inCount); lines.push(` ${resId} [label=${label}];`); } lines.push(''); for (const edge of edges) { const key = edgeKey(edge); const color = edgeColors.get(key) ?? '#5aa7ff'; const isRanking = rankingKeys.has(key); const hp = headPort.get(key) ?? 'in0'; const tc = tailCompass.get(key) ?? 'e'; const attrs = [ `color="${color}"`, `fontcolor="${color}"`, `tooltip="${escapeHtml(edge.tooltip)}"`, `label=" ${escapeHtml(edge.label)} "`, ]; if (!isRanking && edge.from !== edge.to) { attrs.push('constraint=false', 'weight=1'); } else { attrs.push('weight=3'); } // Source port from endpoint if available, else exit compass const fromPort = edge.port ? `:${edge.port}:${tc}` : `:${tc}`; const toPort = `:${hp}:n`; lines.push(` ${edge.from}${fromPort} -> ${edge.to}${toPort} [${attrs.join(', ')}];`); } lines.push('}'); return lines.join('\n'); } function parsePlainPositions(plain) { const positions = new Map(); for (const line of plain.split('\n')) { if (!line.startsWith('node ')) continue; const match = line.match(/^node\s+(\S+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)/); if (!match) continue; positions.set(match[1], { x: Number(match[2]), y: Number(match[3]), width: Number(match[4]), height: Number(match[5]), }); } return positions; } function fixSelfLoopSvg(svg) { return svg.replace( /\s*(\w+)(?::\w+)?->\2(?::\w+)?<\/title>\s*<g id="(a_edge\d+)"><a ([^>]+)>\s*<path([^>]*) d="([^"]+)"\/>\s*<polygon([^>]*) points="([^"]+)"\/>\s*<\/a>\s*<\/g>\s*<\/g>/g, (match, edgeId, _table, aId, aAttrs, pathAttrs, d, polyAttrs) => { const pts = [...d.matchAll(/([-\d.]+)[,\s]+([-\d.]+)/g)].map((m) => [Number(m[1]), Number(m[2])]); if (pts.length < 2) return match; const [x0, y0] = pts[0]; const [x1, y1] = pts[pts.length - 1]; const east = Math.max(x0, x1) + 36; const north = Math.min(y0, y1) - 28; const tx = x1 - east; const ty = y1 - north; const len = Math.hypot(tx, ty) || 1; const dx = tx / len; const dy = ty / len; const arrowLen = 9; const arrowHalf = 3.5; const baseX = x1 - dx * arrowLen; const baseY = y1 - dy * arrowLen; const px = -dy * arrowHalf; const py = dx * arrowHalf; const fmt = (n) => (Math.round(n * 100) / 100).toString(); const newDFmt = `M${fmt(x0)},${fmt(y0)}C${fmt(east)},${fmt(y0)} ${fmt(east)},${fmt(north)} ${fmt(baseX)},${fmt(baseY)}`; const newPointsFmt = `${fmt(x1)},${fmt(y1)} ` + `${fmt(baseX + px)},${fmt(baseY + py)} ` + `${fmt(baseX - px)},${fmt(baseY - py)} ` + `${fmt(x1)},${fmt(y1)}`; return ( `<g id="${edgeId}" class="edge">\n` + `<title>${_table}->${_table}\n` + `\n` + `\n` + `\n` + `\n` + `\n` + `` ); } ); } console.log('Generating Dash API Graph…'); const spec = getOpenApiSpec(true); const resources = parseApi(spec); // Add Legend pseudo-resource resources.set('Legend', { id: 'Legend', basePath: '', endpoints: [], }); const edges = buildApiEdges(resources); console.log(`Parsed ${resources.size - 1} API resources and ${edges.length} relationships`); const graphviz = await Graphviz.load(); // Pass 1: Initial layout to learn coordinates console.log('Layout pass 1/2: computing node positions…'); const initialDot = buildDot(resources, edges, null, null); const plain = graphviz.layout(initialDot, 'plain', 'dot'); const positions = parsePlainPositions(plain); // Pass 2: Anchor assignment using learned positions console.log('Layout pass 2/2: assigning header arrival ports & exit directions…'); const anchors = assignEdgeAnchors(edges, positions); const finalDot = buildDot(resources, edges, positions, anchors); const rawSvg = graphviz.layout(finalDot, 'svg', 'dot'); const fixedSvg = fixSelfLoopSvg(rawSvg); // Ensure background color is set on svg tag const finalSvg = fixedSvg.replace( /]*?)>/, `` ); writeFileSync(dotOutputPath, finalDot, 'utf8'); writeFileSync(outputPath, finalSvg, 'utf8'); const dim = finalSvg.match(/width="([\d.]+)pt" height="([\d.]+)pt"/); console.log(`Written ${dotOutputPath}`); console.log(`Written ${outputPath}`); if (dim) { console.log(`Layout ${dim[1]}pt × ${dim[2]}pt`); }