@eventcatalog/core 5.4.2 → 5.5.1
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/catalog-runtime.cjs +1 -1
- package/dist/{chunk-VSBMYXLL.js → chunk-6KR5ZLEQ.js} +1 -1
- package/dist/{chunk-XB6FJMI3.js → chunk-EBZQJP7F.js} +1 -1
- package/dist/{chunk-HV4ONKTM.js → chunk-GPIKYBQ5.js} +1 -1
- package/dist/{chunk-EXRIIYEP.js → chunk-GZLVDQPM.js} +1 -1
- package/dist/{chunk-CQ4L4SMB.js → chunk-K5QKWVI3.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +14 -14
- package/dist/federation/federate.js +3 -3
- 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 +5 -0
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +5 -3
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +6 -5
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +6 -5
- 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 +80 -68
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +87 -73
- package/eventcatalog/src/features/mcp/mcp-apps.ts +1 -1
- package/eventcatalog/src/features/mcp/mcp-scoped-tools.ts +2 -1
- package/eventcatalog/src/features/mcp/mcp-server.ts +14 -12
- package/eventcatalog/src/features/mcp/schema-viewer.ts +3 -3
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
- package/eventcatalog/src/features/tools/catalog-tools.ts +143 -75
- 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/package.json +5 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -375
- package/dist/{chunk-QJ7KKN2D.js → chunk-NCEZDQSW.js} +7 -7
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API endpoint to return mermaid diagram code for a level of a system diagram
|
|
3
|
+
* URL: /visualiser/systems/{id}/{version}/{overview|services}.mermaid
|
|
4
|
+
*
|
|
5
|
+
* `overview` is level 1 (domains, systems and their relationships) and `services` level 2
|
|
6
|
+
* (services and data stores), as on the Diagram page. {version}.mermaid is the full diagram (level 3).
|
|
7
|
+
*/
|
|
8
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
9
|
+
import { isAuthEnabled } from '@utils/feature';
|
|
10
|
+
import { createMermaidDiagramResponse, getMermaidLevelPaths } from '../../../_mermaid-route';
|
|
11
|
+
|
|
12
|
+
// Prerender static pages when auth is disabled, use SSR when auth is enabled
|
|
13
|
+
export const prerender = !isAuthEnabled();
|
|
14
|
+
|
|
15
|
+
export const getStaticPaths: GetStaticPaths = () => getMermaidLevelPaths('systems');
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ params }) =>
|
|
18
|
+
createMermaidDiagramResponse({ type: 'systems', id: params.id, version: params.version, detail: params.level });
|
|
@@ -21,7 +21,13 @@ export type ArchitectureDiagramCollection = (typeof ARCHITECTURE_DIAGRAM_COLLECT
|
|
|
21
21
|
export const isArchitectureDiagramCollection = (collection: string): collection is ArchitectureDiagramCollection =>
|
|
22
22
|
(ARCHITECTURE_DIAGRAM_COLLECTIONS as readonly string[]).includes(collection);
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The levels of a diagram, as on the Diagram page's toolbar: L1 `overview` (domains, systems and their
|
|
26
|
+
* relationships), L2 `services` (services and data stores, without messages and channels) and L3 `full`.
|
|
27
|
+
*/
|
|
28
|
+
export const ARCHITECTURE_DIAGRAM_DETAILS = ['overview', 'services', 'full'] as const;
|
|
29
|
+
|
|
30
|
+
export type ArchitectureDiagramDetail = (typeof ARCHITECTURE_DIAGRAM_DETAILS)[number];
|
|
25
31
|
|
|
26
32
|
export type Graph = { nodes: any[]; edges: any[] };
|
|
27
33
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* diagram tool used by AI Chat and the MCP server, so they show the same thing.
|
|
5
5
|
*/
|
|
6
6
|
import config from '@config';
|
|
7
|
+
import { hideMessageNodes } from '@eventcatalog/visualiser/layout';
|
|
7
8
|
import { getNodesAndEdges as getNodesAndEdgesForService } from '@utils/node-graphs/services-node-graph';
|
|
8
9
|
import { getNodesAndEdges as getNodesAndEdgesForAgent } from '@utils/node-graphs/agents-node-graph';
|
|
9
10
|
import {
|
|
@@ -79,6 +80,8 @@ export async function getArchitectureDiagramGraph({
|
|
|
79
80
|
}): Promise<Graph> {
|
|
80
81
|
const view = await getArchitectureDiagramView(resource);
|
|
81
82
|
// A domain without systems or subdomains has no level 1, so it shows the full diagram
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
if (detail === 'overview' && view.overview) return { nodes: view.overview.nodes, edges: view.overview.edges };
|
|
84
|
+
// Level 2: as laid out for domains and systems, otherwise the diagram without its messages and channels
|
|
85
|
+
if (detail === 'services') return view.hiddenMessages ?? hideMessageNodes(view.nodes, view.edges);
|
|
86
|
+
return { nodes: view.nodes, edges: view.edges };
|
|
84
87
|
}
|
|
@@ -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
|
|
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.1",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@modelcontextprotocol/ext-apps": "2.0.3",
|
|
70
70
|
"@modelcontextprotocol/server": "2.3.1",
|
|
71
71
|
"@nanostores/react": "^1.0.0",
|
|
72
|
-
"@parcel/watcher": "^2.4
|
|
72
|
+
"@parcel/watcher": "^2.5.4",
|
|
73
73
|
"@radix-ui/react-context-menu": "^2.2.6",
|
|
74
74
|
"@radix-ui/react-dialog": "^1.1.6",
|
|
75
75
|
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
|
@@ -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",
|
|
145
144
|
"@eventcatalog/fitness": "0.1.0",
|
|
146
145
|
"@eventcatalog/linter": "1.2.4",
|
|
147
146
|
"@eventcatalog/sdk": "2.31.2",
|
|
148
|
-
"@eventcatalog/
|
|
147
|
+
"@eventcatalog/exporters": "0.1.2",
|
|
148
|
+
"@eventcatalog/visualiser": "^5.1.0"
|
|
149
149
|
},
|
|
150
150
|
"devDependencies": {
|
|
151
151
|
"@astrojs/check": "^0.9.10",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"prettier": "^3.3.3",
|
|
172
172
|
"prettier-plugin-astro": "^0.14.1",
|
|
173
173
|
"tsup": "^8.1.0",
|
|
174
|
-
"vite": "^8.1.
|
|
174
|
+
"vite": "^8.1.6",
|
|
175
175
|
"vite-tsconfig-paths": "^4.3.2",
|
|
176
176
|
"vitest": "^4.0.18"
|
|
177
177
|
},
|