@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
|
@@ -2,7 +2,15 @@ import { getDomains } from '@utils/collections/domains';
|
|
|
2
2
|
import { getNodesAndEdges as getSystemNodesAndEdges } from './systems-node-graph';
|
|
3
3
|
import { getNodesAndEdges as getServiceNodesAndEdges } from './services-node-graph';
|
|
4
4
|
import { getNodesAndEdgesForDomainSystems as getDomainContextNodesAndEdges } from './system-context-node-graph';
|
|
5
|
-
import {
|
|
5
|
+
import { getServices } from '@utils/collections/services';
|
|
6
|
+
import { getAgents } from '@utils/collections/agents';
|
|
7
|
+
import { createTriggerMessageNode } from './message-node-graph';
|
|
8
|
+
import {
|
|
9
|
+
createEdge,
|
|
10
|
+
generateIdForNode,
|
|
11
|
+
getEdgeLabelForMessageAsSource,
|
|
12
|
+
getEdgeLabelForServiceAsTarget,
|
|
13
|
+
} from '@utils/node-graphs/utils/utils';
|
|
6
14
|
import { layoutLevels } from '@utils/node-graphs/layout-node-graph';
|
|
7
15
|
import { getMessagesLabel, hideMessageNodes, isCarrier } from '@eventcatalog/visualiser/layout';
|
|
8
16
|
|
|
@@ -27,6 +35,14 @@ const CONTEXT_EDGE_PREFIX = 'context-';
|
|
|
27
35
|
const domainGroupId = (domain: any) => `domain-group-${domain.data.id}-${domain.data.version}`;
|
|
28
36
|
const domainNodeId = (domain: any) => `domain-${domain.data.id}-${domain.data.version}`;
|
|
29
37
|
|
|
38
|
+
/** Whether a node is inside a group (a domain or system), at any depth */
|
|
39
|
+
const isInside = (nodes: Map<string, any>, nodeId: string, groupId: string) => {
|
|
40
|
+
for (let node = nodes.get(nodeId); node?.parentId; node = nodes.get(node.parentId)) {
|
|
41
|
+
if (node.parentId === groupId) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
|
|
30
46
|
/** A system shown as a single node (not expanded into what's inside it) */
|
|
31
47
|
const createSystemNode = (system: any, parentId?: string) => ({
|
|
32
48
|
id: generateIdForNode(system),
|
|
@@ -78,13 +94,17 @@ const createDomainCard = (domain: any, { parentId, subdomain = false }: { parent
|
|
|
78
94
|
* receive them from. Everything inside a system collapses into the system, and
|
|
79
95
|
* anything else in a (sub)domain (e.g. a service not in a system) into the
|
|
80
96
|
* domain, so edges connect domains, systems and actors. A subdomain without
|
|
81
|
-
* systems is shown as a card.
|
|
97
|
+
* systems is shown as a card. Messages a (sub)domain sends or receives itself
|
|
98
|
+
* (`domainMessages`: by message, the boxes drawn with it) fold into it, as a
|
|
99
|
+
* system's do into the system, when only one does (more than one, they connect
|
|
100
|
+
* through the message instead, rather than to each other).
|
|
82
101
|
*/
|
|
83
102
|
export const collapseSystems = (
|
|
84
103
|
graph: Graph,
|
|
85
104
|
systems: any[],
|
|
86
105
|
contextNodes: Map<string, any>,
|
|
87
|
-
subdomains: Map<string, any
|
|
106
|
+
subdomains: Map<string, any>,
|
|
107
|
+
domainMessages: Map<string, Set<string>> = new Map()
|
|
88
108
|
): Graph => {
|
|
89
109
|
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
90
110
|
const systemsByGroupId = new Map<string, any>(systems.map((system: any) => [systemGroupId(system), system]));
|
|
@@ -106,6 +126,8 @@ export const collapseSystems = (
|
|
|
106
126
|
// The node something is shown as: its system, the card of the subdomain it's
|
|
107
127
|
// in, the (sub)domain it's in, or itself
|
|
108
128
|
const representative = (nodeId: string): string => {
|
|
129
|
+
const boxes = domainMessages.get(nodeId);
|
|
130
|
+
if (boxes?.size === 1) return representative([...boxes][0]);
|
|
109
131
|
for (let node = nodesById.get(nodeId); node; node = node.parentId ? nodesById.get(node.parentId) : undefined) {
|
|
110
132
|
const system: any = systemsByGroupId.get(node.id);
|
|
111
133
|
if (system) return generateIdForNode(system);
|
|
@@ -126,13 +148,6 @@ export const collapseSystems = (
|
|
|
126
148
|
(node) => representative(node.id) === node.id && !systemsByGroupId.has(node.id) && node.type !== 'data'
|
|
127
149
|
);
|
|
128
150
|
const shownNodes = new Map([...otherNodes, ...systemNodes, ...cards.values()].map((node) => [node.id, node]));
|
|
129
|
-
// Whether one shown node is inside another (e.g. a system in its domain)
|
|
130
|
-
const isInside = (nodeId: string, groupId: string) => {
|
|
131
|
-
for (let node = shownNodes.get(nodeId); node?.parentId; node = shownNodes.get(node.parentId)) {
|
|
132
|
-
if (node.parentId === groupId) return true;
|
|
133
|
-
}
|
|
134
|
-
return false;
|
|
135
|
-
};
|
|
136
151
|
|
|
137
152
|
// Edges between systems / actors / domains: context relationships as they
|
|
138
153
|
// are, edges to nodes kept as their first label, the rest labelled with the
|
|
@@ -148,8 +163,9 @@ export const collapseSystems = (
|
|
|
148
163
|
source === target ||
|
|
149
164
|
!shownNodes.has(source) ||
|
|
150
165
|
!shownNodes.has(target) ||
|
|
151
|
-
|
|
152
|
-
isInside(
|
|
166
|
+
// One shown node inside another (e.g. a system in its domain)
|
|
167
|
+
isInside(shownNodes, source, target) ||
|
|
168
|
+
isInside(shownNodes, target, source)
|
|
153
169
|
)
|
|
154
170
|
return;
|
|
155
171
|
const key = `${source}->${target}`;
|
|
@@ -244,6 +260,8 @@ export const buildDomainGraph = ({
|
|
|
244
260
|
looseServices,
|
|
245
261
|
serviceGraphs,
|
|
246
262
|
mode,
|
|
263
|
+
domains,
|
|
264
|
+
catalogServices,
|
|
247
265
|
}: {
|
|
248
266
|
tree: Tree;
|
|
249
267
|
owners: Owners;
|
|
@@ -252,7 +270,11 @@ export const buildDomainGraph = ({
|
|
|
252
270
|
systemGraphs: Graph[];
|
|
253
271
|
looseServices: { service: any; groupId: string }[];
|
|
254
272
|
serviceGraphs: Graph[];
|
|
255
|
-
mode:
|
|
273
|
+
mode: 'simple' | 'full';
|
|
274
|
+
/** Every domain, for the ones sending or receiving the tree's domain-level messages themselves */
|
|
275
|
+
domains: any[];
|
|
276
|
+
/** Every service and agent, for the producers and consumers of the tree's domain-level messages */
|
|
277
|
+
catalogServices: any[];
|
|
256
278
|
}) => {
|
|
257
279
|
const groupOfDomain = new Map(tree.map((entry) => [entry.domain.data.id, entry.groupId]));
|
|
258
280
|
const groupOfSystem = new Map(systems.map(({ system, groupId }) => [systemGroupId(system), groupId]));
|
|
@@ -399,23 +421,199 @@ export const buildDomainGraph = ({
|
|
|
399
421
|
});
|
|
400
422
|
});
|
|
401
423
|
|
|
424
|
+
const { otherDomainIds, domainMessages } = addDomainMessages({
|
|
425
|
+
tree,
|
|
426
|
+
owners,
|
|
427
|
+
domains,
|
|
428
|
+
catalogServices,
|
|
429
|
+
nodes,
|
|
430
|
+
edges,
|
|
431
|
+
addOtherDomain,
|
|
432
|
+
mode,
|
|
433
|
+
});
|
|
434
|
+
|
|
402
435
|
// Other domains shown as a box (around their subdomains) rather than a card
|
|
403
436
|
replacedBy.forEach((nodeId, replaced) => {
|
|
404
437
|
const boxId = cardBoxes.get(nodeId);
|
|
405
438
|
if (boxId) replacedBy.set(replaced, boxId);
|
|
406
439
|
});
|
|
407
440
|
|
|
408
|
-
const graphEdges = connectEdges(edges, replacedBy, nodes);
|
|
409
|
-
placeMessagesWithPublishers(nodes, graphEdges);
|
|
410
|
-
return {
|
|
441
|
+
const graphEdges = connectEdges(edges, new Map([...cardBoxes, ...replacedBy]), nodes);
|
|
442
|
+
placeMessagesWithPublishers(nodes, graphEdges, new Set(domainMessages.keys()));
|
|
443
|
+
return {
|
|
444
|
+
nodes,
|
|
445
|
+
edges: graphEdges,
|
|
446
|
+
otherDomainIds: new Set([...replacedBy.values(), ...[...otherDomainIds].map((id) => cardBoxes.get(id) ?? id)]),
|
|
447
|
+
domainMessages,
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Messages domains send or receive themselves (`sends` and `receives` in their
|
|
453
|
+
* frontmatter). The domain's (and its subdomains') are connected to its box,
|
|
454
|
+
* outside it, and joined to the other side outside it: services in other
|
|
455
|
+
* domains (shown as those domains) and services in no domain. A message one of
|
|
456
|
+
* the domain's services already sends (or receives) is left as it is. Other
|
|
457
|
+
* domains sending a message the domain receives themselves (or receiving one it
|
|
458
|
+
* sends) are connected to it, as those domains. Returns the other domains added
|
|
459
|
+
* and, by message, the (sub)domain boxes it's drawn sent from or received by.
|
|
460
|
+
*/
|
|
461
|
+
const addDomainMessages = ({
|
|
462
|
+
tree,
|
|
463
|
+
owners,
|
|
464
|
+
domains,
|
|
465
|
+
catalogServices,
|
|
466
|
+
nodes,
|
|
467
|
+
edges,
|
|
468
|
+
addOtherDomain,
|
|
469
|
+
mode,
|
|
470
|
+
}: {
|
|
471
|
+
tree: Tree;
|
|
472
|
+
owners: Owners;
|
|
473
|
+
domains: any[];
|
|
474
|
+
catalogServices: any[];
|
|
475
|
+
nodes: Map<string, any>;
|
|
476
|
+
edges: any[];
|
|
477
|
+
addOtherDomain: (domain: any) => string;
|
|
478
|
+
mode: 'simple' | 'full';
|
|
479
|
+
}) => {
|
|
480
|
+
const treeDomainIds = new Set(tree.map((entry) => entry.domain.data.id));
|
|
481
|
+
const otherDomainIds = new Set<string>();
|
|
482
|
+
const domainMessages = new Map<string, Set<string>>();
|
|
483
|
+
// The domain each agent is in (owners only has services): the tree's versions first
|
|
484
|
+
const agentOwners = new Map<string, any>();
|
|
485
|
+
[...tree.map((entry) => entry.domain), ...domains].forEach((domain) =>
|
|
486
|
+
(domain.data.agents || []).forEach((agent: any) => {
|
|
487
|
+
const agentId = generateIdForNode(agent);
|
|
488
|
+
if (!agentOwners.has(agentId)) agentOwners.set(agentId, domain);
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
// Whether a domain or service sends (or receives) the message (their messages are already looked up)
|
|
492
|
+
const lists = (resource: any, key: 'sends' | 'receives', messageId: string) =>
|
|
493
|
+
(resource.data[key] || []).some((message: any) => message.data && generateIdForNode(message) === messageId);
|
|
494
|
+
const addOther = (domain: any) => {
|
|
495
|
+
const nodeId = addOtherDomain(domain);
|
|
496
|
+
otherDomainIds.add(nodeId);
|
|
497
|
+
return nodeId;
|
|
498
|
+
};
|
|
499
|
+
// Whether a message's senders (or receivers) include something in a (sub)domain's box: a service, or the box
|
|
500
|
+
// itself, at the other end of its edges, through any channels
|
|
501
|
+
const carries = (nodeId: string) => nodes.has(nodeId) && isCarrier(nodes.get(nodeId));
|
|
502
|
+
const reaches = (messageId: string, direction: 'senders' | 'receivers', groupId: string) => {
|
|
503
|
+
const seen = new Set([messageId]);
|
|
504
|
+
for (let current = [messageId]; current.length > 0; ) {
|
|
505
|
+
const next = edges
|
|
506
|
+
.filter((edge) => current.includes(direction === 'receivers' ? edge.source : edge.target))
|
|
507
|
+
.map((edge) => (direction === 'receivers' ? edge.target : edge.source))
|
|
508
|
+
.filter((nodeId) => !seen.has(nodeId));
|
|
509
|
+
next.forEach((nodeId) => seen.add(nodeId));
|
|
510
|
+
if (next.some((nodeId) => !carries(nodeId) && (nodeId === groupId || isInside(nodes, nodeId, groupId)))) return true;
|
|
511
|
+
current = next.filter(carries);
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
};
|
|
515
|
+
// Whether a service in a (sub)domain's box says it sends (or receives) the message: e.g. one it sends in a
|
|
516
|
+
// message group, which its graph shows as the group rather than the message. Services only: agents in a
|
|
517
|
+
// domain aren't drawn on its diagram
|
|
518
|
+
const groupOfTreeDomain = new Map(tree.map((entry) => [entry.domain.data.id, entry.groupId]));
|
|
519
|
+
const declaredIn = (groupId: string, key: 'sends' | 'receives', messageId: string) =>
|
|
520
|
+
catalogServices.some((service) => {
|
|
521
|
+
if (!lists(service, key, messageId)) return false;
|
|
522
|
+
const ownerGroup = groupOfTreeDomain.get(owners.get(generateIdForNode(service))?.domain.data.id);
|
|
523
|
+
return !!ownerGroup && (ownerGroup === groupId || isInside(nodes, ownerGroup, groupId));
|
|
524
|
+
});
|
|
525
|
+
// The node standing for a service (or agent) on the other side, or none when it's in the domain
|
|
526
|
+
const otherSide = (service: any) => {
|
|
527
|
+
const serviceId = generateIdForNode(service);
|
|
528
|
+
const ownerDomain = owners.get(serviceId)?.domain ?? agentOwners.get(serviceId);
|
|
529
|
+
if (ownerDomain) return treeDomainIds.has(ownerDomain.data.id) ? undefined : addOther(ownerDomain);
|
|
530
|
+
if (!nodes.has(serviceId)) {
|
|
531
|
+
nodes.set(serviceId, {
|
|
532
|
+
id: serviceId,
|
|
533
|
+
type: service.collection,
|
|
534
|
+
position: { x: 0, y: 0 },
|
|
535
|
+
data: {
|
|
536
|
+
mode,
|
|
537
|
+
...(service.collection === 'agents' ? { agent: { ...service.data } } : { service: { ...service.data } }),
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return serviceId;
|
|
542
|
+
};
|
|
543
|
+
const sendEdge = (source: string, message: any) =>
|
|
544
|
+
createEdge({
|
|
545
|
+
id: `domain-sends-${source}-${generateIdForNode(message)}`,
|
|
546
|
+
source,
|
|
547
|
+
target: generateIdForNode(message),
|
|
548
|
+
label: getEdgeLabelForServiceAsTarget(message),
|
|
549
|
+
});
|
|
550
|
+
const receiveEdge = (message: any, target: string) =>
|
|
551
|
+
createEdge({
|
|
552
|
+
id: `domain-receives-${generateIdForNode(message)}-${target}`,
|
|
553
|
+
source: generateIdForNode(message),
|
|
554
|
+
target,
|
|
555
|
+
label: getEdgeLabelForMessageAsSource(message),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
tree.forEach(({ domain, groupId }) => {
|
|
559
|
+
(['sends', 'receives'] as const).forEach((key) =>
|
|
560
|
+
(domain.data[key] || []).forEach((message: any) => {
|
|
561
|
+
if (!message.data) return;
|
|
562
|
+
const messageId = generateIdForNode(message);
|
|
563
|
+
const sends = key === 'sends';
|
|
564
|
+
if (reaches(messageId, sends ? 'senders' : 'receivers', groupId) || declaredIn(groupId, key, messageId)) return;
|
|
565
|
+
// Outside the box, as the box sends or receives it (e.g. not left in it by a service in it consuming it)
|
|
566
|
+
const node = nodes.get(messageId);
|
|
567
|
+
if (!node) nodes.set(messageId, createTriggerMessageNode(message, mode));
|
|
568
|
+
else if (isInside(nodes, messageId, groupId)) nodes.set(messageId, { ...node, parentId: undefined, extent: undefined });
|
|
569
|
+
domainMessages.set(messageId, (domainMessages.get(messageId) ?? new Set()).add(groupId));
|
|
570
|
+
edges.push(sends ? sendEdge(groupId, message) : receiveEdge(message, groupId));
|
|
571
|
+
|
|
572
|
+
// Who receives what the domain sends, and who sends what it receives
|
|
573
|
+
new Set(
|
|
574
|
+
catalogServices
|
|
575
|
+
.filter((service) => lists(service, sends ? 'receives' : 'sends', messageId))
|
|
576
|
+
.map(otherSide)
|
|
577
|
+
.filter((nodeId): nodeId is string => !!nodeId)
|
|
578
|
+
).forEach((nodeId) => edges.push(sends ? receiveEdge(message, nodeId) : sendEdge(nodeId, message)));
|
|
579
|
+
})
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Other domains sending a message the domain receives themselves, or receiving one it sends
|
|
584
|
+
const listed = new Map<string, { message: any; sends: any[]; receives: any[] }>();
|
|
585
|
+
domains
|
|
586
|
+
.filter((domain) => !treeDomainIds.has(domain.data.id))
|
|
587
|
+
.forEach((domain) =>
|
|
588
|
+
(['sends', 'receives'] as const).forEach((key) =>
|
|
589
|
+
(domain.data[key] || []).forEach((message: any) => {
|
|
590
|
+
if (!message.data) return;
|
|
591
|
+
const messageId = generateIdForNode(message);
|
|
592
|
+
const entry = listed.get(messageId) ?? { message, sends: [], receives: [] };
|
|
593
|
+
entry[key].push(domain);
|
|
594
|
+
listed.set(messageId, entry);
|
|
595
|
+
})
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
const rootGroupId = tree[0].groupId;
|
|
599
|
+
listed.forEach(({ message, sends, receives }, messageId) => {
|
|
600
|
+
if (!MESSAGE_TYPES.has(nodes.get(messageId)?.type)) return;
|
|
601
|
+
if (sends.length && reaches(messageId, 'receivers', rootGroupId))
|
|
602
|
+
sends.forEach((domain) => edges.push(sendEdge(addOther(domain), message)));
|
|
603
|
+
if (receives.length && reaches(messageId, 'senders', rootGroupId))
|
|
604
|
+
receives.forEach((domain) => edges.push(receiveEdge(message, addOther(domain))));
|
|
605
|
+
});
|
|
606
|
+
return { otherDomainIds, domainMessages };
|
|
411
607
|
};
|
|
412
608
|
|
|
413
609
|
/**
|
|
414
610
|
* Messages are placed in the first graph they appear in, which can be a
|
|
415
611
|
* system consuming them. Moves each message into the system (or domain) of the
|
|
416
|
-
* services publishing it, when they're all in the same one.
|
|
612
|
+
* services publishing it, when they're all in the same one. Messages a domain
|
|
613
|
+
* sends or receives itself (`kept`) stay outside its box, where its edge to them
|
|
614
|
+
* is drawn.
|
|
417
615
|
*/
|
|
418
|
-
export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[]) => {
|
|
616
|
+
export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[], kept: Set<string> = new Set()) => {
|
|
419
617
|
const publisherGroups = new Map<string, Set<string | undefined>>();
|
|
420
618
|
edges.forEach((edge) => {
|
|
421
619
|
const publisher = nodes.get(edge.source);
|
|
@@ -425,6 +623,7 @@ export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[
|
|
|
425
623
|
publisherGroups.set(edge.target, groups);
|
|
426
624
|
});
|
|
427
625
|
publisherGroups.forEach((groups, messageId) => {
|
|
626
|
+
if (kept.has(messageId)) return;
|
|
428
627
|
const [parentId] = [...groups];
|
|
429
628
|
const message = nodes.get(messageId);
|
|
430
629
|
if (groups.size === 1 && parentId && message.parentId !== parentId) {
|
|
@@ -438,12 +637,6 @@ export const placeMessagesWithPublishers = (nodes: Map<string, any>, edges: any[
|
|
|
438
637
|
* nowhere and ones between a domain and a subdomain in it.
|
|
439
638
|
*/
|
|
440
639
|
export const connectEdges = (edges: any[], replacedBy: Map<string, string>, nodes: Map<string, any>) => {
|
|
441
|
-
const isInside = (nodeId: string, groupId: string) => {
|
|
442
|
-
for (let node = nodes.get(nodeId); node?.parentId; node = nodes.get(node.parentId)) {
|
|
443
|
-
if (node.parentId === groupId) return true;
|
|
444
|
-
}
|
|
445
|
-
return false;
|
|
446
|
-
};
|
|
447
640
|
const seen = new Set<string>();
|
|
448
641
|
return edges
|
|
449
642
|
.map((edge) => ({
|
|
@@ -457,8 +650,8 @@ export const connectEdges = (edges: any[], replacedBy: Map<string, string>, node
|
|
|
457
650
|
edge.source === edge.target ||
|
|
458
651
|
!nodes.has(edge.source) ||
|
|
459
652
|
!nodes.has(edge.target) ||
|
|
460
|
-
isInside(edge.source, edge.target) ||
|
|
461
|
-
isInside(edge.target, edge.source) ||
|
|
653
|
+
isInside(nodes, edge.source, edge.target) ||
|
|
654
|
+
isInside(nodes, edge.target, edge.source) ||
|
|
462
655
|
seen.has(key)
|
|
463
656
|
)
|
|
464
657
|
return false;
|
|
@@ -577,13 +770,34 @@ export const getNodesAndEdges = (props: NodesAndEdgesProps): Promise<DomainLevel
|
|
|
577
770
|
return levelsCache.get(key)!;
|
|
578
771
|
};
|
|
579
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Each service and agent in the version a domain has (it can pin an older one),
|
|
775
|
+
* or its latest when no domain has it: who sends or receives a domain's own
|
|
776
|
+
* messages is matched against these.
|
|
777
|
+
*/
|
|
778
|
+
const inVersionsUsed = (resources: any[], domains: any[], owners: Owners) => {
|
|
779
|
+
const used = new Set([
|
|
780
|
+
...owners.keys(),
|
|
781
|
+
...domains.flatMap((domain) => (domain.data.agents || []).map((agent: any) => generateIdForNode(agent))),
|
|
782
|
+
]);
|
|
783
|
+
const key = (resource: any) => `${resource.collection}:${resource.data.id}`;
|
|
784
|
+
const usedKeys = new Set(resources.filter((resource) => used.has(generateIdForNode(resource))).map(key));
|
|
785
|
+
return resources.filter((resource) =>
|
|
786
|
+
usedKeys.has(key(resource))
|
|
787
|
+
? used.has(generateIdForNode(resource))
|
|
788
|
+
: (resource.data.latestVersion ?? resource.data.version) === resource.data.version
|
|
789
|
+
);
|
|
790
|
+
};
|
|
791
|
+
|
|
580
792
|
const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: NodesAndEdgesProps): Promise<DomainLevels> => {
|
|
581
793
|
// Each domain with only its own services (not its subdomains'): the version
|
|
582
794
|
// shown (which may be an older one), and the latest of every domain for its
|
|
583
795
|
// subdomains and the other domains around it
|
|
584
|
-
const [allVersions, domains] = await Promise.all([
|
|
796
|
+
const [allVersions, domains, services, agents] = await Promise.all([
|
|
585
797
|
getDomains({ getAllVersions: true, includeServicesInSubdomains: false }),
|
|
586
798
|
getDomains({ getAllVersions: false, includeServicesInSubdomains: false }),
|
|
799
|
+
getServices({ getAllVersions: true }),
|
|
800
|
+
getAgents({ getAllVersions: true }),
|
|
587
801
|
]);
|
|
588
802
|
const domain = allVersions.find((d) => d.data.id === id && d.data.version === version);
|
|
589
803
|
if (!domain) return { nodes: [], edges: [], hiddenMessages: { nodes: [], edges: [] } };
|
|
@@ -646,7 +860,7 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
646
860
|
),
|
|
647
861
|
]);
|
|
648
862
|
|
|
649
|
-
const { nodes, edges, otherDomainIds } = buildDomainGraph({
|
|
863
|
+
const { nodes, edges, otherDomainIds, domainMessages } = buildDomainGraph({
|
|
650
864
|
tree,
|
|
651
865
|
owners,
|
|
652
866
|
parentDomainOf,
|
|
@@ -655,6 +869,8 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
655
869
|
looseServices,
|
|
656
870
|
serviceGraphs,
|
|
657
871
|
mode,
|
|
872
|
+
domains,
|
|
873
|
+
catalogServices: inVersionsUsed([...services, ...agents], [...domains, ...tree.map((entry) => entry.domain)], owners),
|
|
658
874
|
});
|
|
659
875
|
const context = addContext(contexts, systems, nodes);
|
|
660
876
|
const graph = { nodes: [...nodes.values(), ...context.nodes], edges: [...edges, ...context.edges] };
|
|
@@ -671,7 +887,8 @@ const buildLevels = async ({ id, version, mode = 'full', channelRenderMode }: No
|
|
|
671
887
|
{ ...graph, edges: detailedGraph.edges },
|
|
672
888
|
systems.map(({ system }) => system),
|
|
673
889
|
context.contextNodesById,
|
|
674
|
-
new Map(tree.filter((entry) => entry.parentId).map((entry) => [entry.groupId, entry.domain]))
|
|
890
|
+
new Map(tree.filter((entry) => entry.parentId).map((entry) => [entry.groupId, entry.domain])),
|
|
891
|
+
domainMessages
|
|
675
892
|
);
|
|
676
893
|
const overview = hideMessageNodes(collapsed.nodes, collapsed.edges);
|
|
677
894
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
1
2
|
import { getNodesAndEdges as getSystemContextNodesAndEdges } from './system-context-node-graph';
|
|
2
|
-
import { getNodesAndEdges as getSystemNodesAndEdges } from './systems-node-graph';
|
|
3
|
+
import { getNodesAndEdges as getSystemNodesAndEdges, memberServiceNodeIds, SERVICE_NODE_TYPES } from './systems-node-graph';
|
|
3
4
|
import { layoutLevels } from '@utils/node-graphs/layout-node-graph';
|
|
5
|
+
import { createVersionedMap, findInMap } from '@utils/collections/util';
|
|
4
6
|
|
|
5
7
|
interface NodesAndEdgesProps {
|
|
6
8
|
id: string;
|
|
@@ -22,6 +24,145 @@ export const getNodesAndEdges = (props: NodesAndEdgesProps) => {
|
|
|
22
24
|
return levelsCache.get(key)!;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
const nodeIdOf = (resource: { data: { id: string; version?: string } }) => `${resource.data.id}-${resource.data.version}`;
|
|
28
|
+
|
|
29
|
+
const isSameResource = (a: { data: { id: string; version?: string } }, b: { data: { id: string; version?: string } }) =>
|
|
30
|
+
a.data.id === b.data.id && a.data.version === b.data.version;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every system that lists a service, keyed by the service's resolved node id
|
|
34
|
+
* (`id-version`). Pointers use the same version matching as the rest of the
|
|
35
|
+
* catalog (`latest`, exact versions, semver ranges), so the key is the
|
|
36
|
+
* concrete service the pointer selects, not the pointer text.
|
|
37
|
+
*/
|
|
38
|
+
export const collectServiceOwners = (systems: any[], serviceMap: Map<string, any[]>) => {
|
|
39
|
+
const owners = new Map<string, any[]>();
|
|
40
|
+
for (const system of systems) {
|
|
41
|
+
for (const ref of system.data.services || []) {
|
|
42
|
+
const service = findInMap(serviceMap, ref.id, ref.version);
|
|
43
|
+
if (!service) continue;
|
|
44
|
+
const id = nodeIdOf(service);
|
|
45
|
+
const list = owners.get(id) ?? [];
|
|
46
|
+
if (!list.some((owner) => isSameResource(owner, system))) list.push(system);
|
|
47
|
+
owners.set(id, list);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return owners;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Which of a service's systems stands for it on this diagram.
|
|
55
|
+
* A direct relationship wins, in the order the focused system declares them
|
|
56
|
+
* (their version pointers resolved the same way as the context diagram).
|
|
57
|
+
* Otherwise the lowest system id, then version, so catalog order doesn't matter.
|
|
58
|
+
* Systems that aren't already on the diagram are not candidates: the service
|
|
59
|
+
* is drawn on its own instead of inventing a system box.
|
|
60
|
+
*/
|
|
61
|
+
const chooseOwningSystem = (owners: any[], contextSystemIds: Set<string>, focused: any, systemMap: Map<string, any[]>) => {
|
|
62
|
+
const inGraph = owners.filter((system) => !isSameResource(system, focused) && contextSystemIds.has(nodeIdOf(system)));
|
|
63
|
+
if (inGraph.length === 0) return undefined;
|
|
64
|
+
|
|
65
|
+
const inGraphIds = new Set(inGraph.map(nodeIdOf));
|
|
66
|
+
for (const relationship of focused.data.relationships || []) {
|
|
67
|
+
const related = findInMap(systemMap, relationship.id, relationship.version);
|
|
68
|
+
if (related && inGraphIds.has(nodeIdOf(related))) return related;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [...inGraph].sort((a, b) => {
|
|
72
|
+
const byId = a.data.id.localeCompare(b.data.id);
|
|
73
|
+
return byId !== 0 ? byId : String(a.data.version).localeCompare(String(b.data.version));
|
|
74
|
+
})[0];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const detachFromGroup = (node: any) => {
|
|
78
|
+
const { parentId, extent, ...rest } = node;
|
|
79
|
+
return rest;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const edgePair = (source: string, target: string) => `${source}->${target}`;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Drops edges that repeat a relationship (either direction) and edges that
|
|
86
|
+
* repeat the same source, target and label. Relationship edges themselves stay.
|
|
87
|
+
*/
|
|
88
|
+
export const dedupeEdges = (edges: any[], relationshipPairs: Set<string>) => {
|
|
89
|
+
const seen = new Set<string>();
|
|
90
|
+
return edges.filter((edge) => {
|
|
91
|
+
const isRelationship = String(edge.id).startsWith('context-');
|
|
92
|
+
const pair = edgePair(edge.source, edge.target);
|
|
93
|
+
if (!isRelationship && relationshipPairs.has(pair)) return false;
|
|
94
|
+
if (isRelationship) return true;
|
|
95
|
+
const key = `${pair}->${edge.label ?? ''}`;
|
|
96
|
+
if (seen.has(key)) return false;
|
|
97
|
+
seen.add(key);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Services that belong to another system already on the diagram are removed
|
|
104
|
+
* and their edges point at that system. Services in no system, or whose system
|
|
105
|
+
* isn't on the diagram, are kept but not inside this system's group. A service
|
|
106
|
+
* this system lists stays in the group even when another system lists it too.
|
|
107
|
+
*/
|
|
108
|
+
export const rehomeServicesInOtherSystems = ({
|
|
109
|
+
nodes,
|
|
110
|
+
edges,
|
|
111
|
+
groupId,
|
|
112
|
+
focusedSystem,
|
|
113
|
+
allSystems,
|
|
114
|
+
allServices,
|
|
115
|
+
contextSystemIds,
|
|
116
|
+
relationshipPairs,
|
|
117
|
+
}: {
|
|
118
|
+
nodes: any[];
|
|
119
|
+
edges: any[];
|
|
120
|
+
groupId: string;
|
|
121
|
+
focusedSystem: any;
|
|
122
|
+
allSystems: any[];
|
|
123
|
+
allServices: any[];
|
|
124
|
+
contextSystemIds: Set<string>;
|
|
125
|
+
relationshipPairs: Set<string>;
|
|
126
|
+
}) => {
|
|
127
|
+
const serviceMap = createVersionedMap(allServices);
|
|
128
|
+
const systemMap = createVersionedMap(allSystems);
|
|
129
|
+
const owners = collectServiceOwners(allSystems, serviceMap);
|
|
130
|
+
const members = memberServiceNodeIds(focusedSystem, serviceMap);
|
|
131
|
+
const replacedBy = new Map<string, string>();
|
|
132
|
+
|
|
133
|
+
const nextNodes = nodes
|
|
134
|
+
.map((node) => {
|
|
135
|
+
if (node.id === groupId || !SERVICE_NODE_TYPES.has(node.type)) return node;
|
|
136
|
+
const nodeOwners = owners.get(node.id) ?? [];
|
|
137
|
+
// Listed here, or resolved as a member: it belongs inside this group
|
|
138
|
+
if (members.has(node.id) || nodeOwners.some((system) => isSameResource(system, focusedSystem))) {
|
|
139
|
+
return node.parentId === groupId ? node : { ...node, parentId: groupId, extent: 'parent' };
|
|
140
|
+
}
|
|
141
|
+
const owner = chooseOwningSystem(nodeOwners, contextSystemIds, focusedSystem, systemMap);
|
|
142
|
+
if (owner) {
|
|
143
|
+
replacedBy.set(node.id, nodeIdOf(owner));
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return detachFromGroup(node);
|
|
147
|
+
})
|
|
148
|
+
.filter((node): node is any => !!node);
|
|
149
|
+
|
|
150
|
+
const seen = new Set<string>();
|
|
151
|
+
const nextEdges = edges.flatMap((edge) => {
|
|
152
|
+
const source = replacedBy.get(edge.source) ?? edge.source;
|
|
153
|
+
const target = replacedBy.get(edge.target) ?? edge.target;
|
|
154
|
+
if (source === target) return [];
|
|
155
|
+
const pair = edgePair(source, target);
|
|
156
|
+
if (relationshipPairs.has(pair)) return [];
|
|
157
|
+
const key = `${pair}->${edge.label ?? ''}`;
|
|
158
|
+
if (seen.has(key)) return [];
|
|
159
|
+
seen.add(key);
|
|
160
|
+
return [{ ...edge, source, target }];
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return { nodes: nextNodes, edges: nextEdges, replacedBy };
|
|
164
|
+
};
|
|
165
|
+
|
|
25
166
|
/**
|
|
26
167
|
* A system's resource diagram shown inside its context diagram: the system's node
|
|
27
168
|
* is replaced by the expanded system group (services, data stores, messages...),
|
|
@@ -30,17 +171,27 @@ export const getNodesAndEdges = (props: NodesAndEdgesProps) => {
|
|
|
30
171
|
* its messages and channels (`hiddenMessages`).
|
|
31
172
|
*/
|
|
32
173
|
const buildLevels = async ({ id, version, mode = 'simple', channelRenderMode }: NodesAndEdgesProps) => {
|
|
33
|
-
const [context, system] = await Promise.all([
|
|
174
|
+
const [context, system, allSystems, allServices] = await Promise.all([
|
|
34
175
|
getSystemContextNodesAndEdges({ id, version, mode, layout: false }),
|
|
35
|
-
getSystemNodesAndEdges({
|
|
176
|
+
getSystemNodesAndEdges({
|
|
177
|
+
id,
|
|
178
|
+
version,
|
|
179
|
+
mode,
|
|
180
|
+
channelRenderMode,
|
|
181
|
+
wrapInSystemGroup: true,
|
|
182
|
+
confineToMembers: true,
|
|
183
|
+
layout: false,
|
|
184
|
+
}),
|
|
185
|
+
getCollection('systems'),
|
|
186
|
+
getCollection('services'),
|
|
36
187
|
]);
|
|
37
188
|
|
|
38
189
|
const group = system.nodes.find((node: any) => node.type === 'system-group');
|
|
39
190
|
const rootSystemNode = context.nodes.find((node: any) => node.data?.isFocused);
|
|
40
191
|
if (!group || !rootSystemNode) return layoutLevels(system, context);
|
|
41
192
|
|
|
42
|
-
const
|
|
43
|
-
|
|
193
|
+
const focusedSystem = allSystems.find((systemEntry) => systemEntry.data.id === id && systemEntry.data.version === version);
|
|
194
|
+
|
|
44
195
|
const toGroup = (nodeId: string) => (nodeId === rootSystemNode.id ? group.id : nodeId);
|
|
45
196
|
const contextEdges = context.edges.map((edge: any) => ({
|
|
46
197
|
...edge,
|
|
@@ -48,12 +199,47 @@ const buildLevels = async ({ id, version, mode = 'simple', channelRenderMode }:
|
|
|
48
199
|
source: toGroup(edge.source),
|
|
49
200
|
target: toGroup(edge.target),
|
|
50
201
|
}));
|
|
202
|
+
const relationshipPairs = new Set<string>();
|
|
203
|
+
for (const edge of contextEdges) {
|
|
204
|
+
relationshipPairs.add(edgePair(edge.source, edge.target));
|
|
205
|
+
relationshipPairs.add(edgePair(edge.target, edge.source));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const contextSystemIds = new Set(
|
|
209
|
+
context.nodes.filter((node: any) => node.type === 'systems' && node.id !== rootSystemNode.id).map((node: any) => node.id)
|
|
210
|
+
);
|
|
51
211
|
|
|
52
|
-
|
|
212
|
+
const rehomed = focusedSystem
|
|
213
|
+
? rehomeServicesInOtherSystems({
|
|
214
|
+
nodes: system.nodes,
|
|
215
|
+
edges: system.edges,
|
|
216
|
+
groupId: group.id,
|
|
217
|
+
focusedSystem,
|
|
218
|
+
allSystems,
|
|
219
|
+
allServices,
|
|
220
|
+
contextSystemIds,
|
|
221
|
+
relationshipPairs,
|
|
222
|
+
})
|
|
223
|
+
: { nodes: system.nodes, edges: system.edges };
|
|
224
|
+
|
|
225
|
+
const systemNodeIds = new Set(rehomed.nodes.map((node: any) => node.id));
|
|
226
|
+
const contextNodes = context.nodes.filter((node: any) => node.id !== rootSystemNode.id && !systemNodeIds.has(node.id));
|
|
227
|
+
|
|
228
|
+
// Lay the context and the expanded system out together. Redirects happen
|
|
229
|
+
// before this, so level 2 (messages hidden) bridges onto the system boxes.
|
|
53
230
|
const detailed = {
|
|
54
231
|
// Parent must come before its children in the array for React Flow
|
|
55
|
-
nodes: [group, ...contextNodes, ...
|
|
56
|
-
edges: [...
|
|
232
|
+
nodes: [group, ...contextNodes, ...rehomed.nodes.filter((node: any) => node.id !== group.id)],
|
|
233
|
+
edges: [...rehomed.edges, ...contextEdges],
|
|
234
|
+
};
|
|
235
|
+
const laidOut = await layoutLevels(detailed, context);
|
|
236
|
+
// Hiding messages can bridge two systems that a relationship already joins
|
|
237
|
+
return {
|
|
238
|
+
...laidOut,
|
|
239
|
+
edges: dedupeEdges(laidOut.edges, relationshipPairs),
|
|
240
|
+
hiddenMessages: {
|
|
241
|
+
...laidOut.hiddenMessages,
|
|
242
|
+
edges: dedupeEdges(laidOut.hiddenMessages.edges, relationshipPairs),
|
|
243
|
+
},
|
|
57
244
|
};
|
|
58
|
-
return layoutLevels(detailed, context);
|
|
59
245
|
};
|