@eventcatalog/core 2.44.5 → 2.45.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.
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "2.44.5";
40
+ var version = "2.45.1";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-ULCIAC3E.js";
4
- import "../chunk-WG64IAI3.js";
3
+ } from "../chunk-77HCL77S.js";
4
+ import "../chunk-DDM6QEBV.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -106,7 +106,7 @@ var import_axios = __toESM(require("axios"), 1);
106
106
  var import_os = __toESM(require("os"), 1);
107
107
 
108
108
  // package.json
109
- var version = "2.44.5";
109
+ var version = "2.45.1";
110
110
 
111
111
  // src/constants.ts
112
112
  var VERSION = version;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-WO7XQZNY.js";
4
- import "../chunk-ULCIAC3E.js";
5
- import "../chunk-WG64IAI3.js";
3
+ } from "../chunk-P23CLNWF.js";
4
+ import "../chunk-77HCL77S.js";
5
+ import "../chunk-DDM6QEBV.js";
6
6
  import "../chunk-E7TXTI7G.js";
7
7
  export {
8
8
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-WG64IAI3.js";
3
+ } from "./chunk-DDM6QEBV.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "2.44.5";
2
+ var version = "2.45.1";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-ULCIAC3E.js";
3
+ } from "./chunk-77HCL77S.js";
4
4
  import {
5
5
  getEventCatalogConfigFile,
6
6
  verifyRequiredFieldsAreInCatalogConfigFile
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "2.44.5";
28
+ var version = "2.45.1";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-WG64IAI3.js";
3
+ } from "./chunk-DDM6QEBV.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -157,7 +157,7 @@ var import_axios = __toESM(require("axios"), 1);
157
157
  var import_os = __toESM(require("os"), 1);
158
158
 
159
159
  // package.json
160
- var version = "2.44.5";
160
+ var version = "2.45.1";
161
161
 
162
162
  // src/constants.ts
163
163
  var VERSION = version;
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-DCLTVJDP.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-WO7XQZNY.js";
10
- import "./chunk-ULCIAC3E.js";
9
+ } from "./chunk-P23CLNWF.js";
10
+ import "./chunk-77HCL77S.js";
11
11
  import {
12
12
  catalogToAstro,
13
13
  checkAndConvertMdToMdx
@@ -15,7 +15,7 @@ import {
15
15
  import "./chunk-EXAALOQA.js";
16
16
  import {
17
17
  VERSION
18
- } from "./chunk-WG64IAI3.js";
18
+ } from "./chunk-DDM6QEBV.js";
19
19
  import {
20
20
  isAuthEnabled,
21
21
  isBackstagePluginEnabled,
@@ -10,6 +10,7 @@ import {
10
10
  getNodesAndEdges as getNodesAndEdgesForDomain,
11
11
  getNodesAndEdgesForDomainContextMap,
12
12
  } from '@utils/node-graphs/domains-node-graph';
13
+ import { getDomainsCanvasData } from '@utils/node-graphs/domains-canvas';
13
14
  import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
14
15
  import { buildUrl } from '@utils/url-builder';
15
16
  import { getVersionFromCollection } from '@utils/collections/versions';
@@ -89,6 +90,12 @@ if (collection === 'domain-context-map') {
89
90
  nodes = fetchedNodes;
90
91
  edges = fetchedEdges;
91
92
  }
93
+
94
+ if (collection === 'domains-canvas') {
95
+ const { domainNodes, messageNodes, edges: fetchedEdges } = await getDomainsCanvasData();
96
+ nodes = [...domainNodes, ...messageNodes];
97
+ edges = fetchedEdges;
98
+ }
92
99
  ---
93
100
 
94
101
  <div>
@@ -28,6 +28,7 @@ import UserNode from './Nodes/User';
28
28
  import StepNode from './Nodes/Step';
29
29
  import CommandNode from './Nodes/Command';
30
30
  import ExternalSystemNode from './Nodes/ExternalSystem';
31
+ import DomainNode from './Nodes/Domain';
31
32
  import AnimatedMessageEdge from './Edges/AnimatedMessageEdge';
32
33
  import FlowEdge from './Edges/FlowEdge';
33
34
  import CustomNode from './Nodes/Custom';
@@ -78,6 +79,7 @@ const NodeGraphBuilder = ({
78
79
  channels: ChannelNode,
79
80
  queries: QueryNode,
80
81
  commands: CommandNode,
82
+ domains: DomainNode,
81
83
  step: StepNode,
82
84
  user: UserNode,
83
85
  actor: UserNode,
@@ -0,0 +1,155 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import { Handle, useReactFlow, useOnSelectionChange, Position } from '@xyflow/react';
3
+ import * as ContextMenu from '@radix-ui/react-context-menu';
4
+ import { buildUrl } from '@utils/url-builder';
5
+ import { getIcon } from '@utils/badges';
6
+ import { useState } from 'react';
7
+
8
+ interface Data {
9
+ mode: 'simple' | 'full';
10
+ domain: CollectionEntry<'domains'>;
11
+ }
12
+
13
+ export default function DomainNode({ data, id: nodeId }: any) {
14
+ const { mode, domain } = data as Data;
15
+ const reactFlow = useReactFlow();
16
+ const [highlightedServices, setHighlightedServices] = useState<Set<string>>(new Set());
17
+
18
+ const { id, version, name, services = [], styles } = domain.data;
19
+ const { icon = 'RectangleGroupIcon' } = styles || {};
20
+
21
+ const Icon = getIcon(icon);
22
+ const ServerIcon = getIcon('ServerIcon');
23
+
24
+ // Listen for selection changes to highlight connected services
25
+ useOnSelectionChange({
26
+ onChange: ({ nodes: selectedNodes }) => {
27
+ if (selectedNodes.length === 0) {
28
+ setHighlightedServices(new Set());
29
+ return;
30
+ }
31
+
32
+ const selectedNode = selectedNodes[0];
33
+ if (!selectedNode) {
34
+ setHighlightedServices(new Set());
35
+ return;
36
+ }
37
+
38
+ // Get all edges
39
+ const edges = reactFlow.getEdges();
40
+ const connectedServiceIds = new Set<string>();
41
+
42
+ // Find services connected to the selected node
43
+ edges.forEach((edge) => {
44
+ if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
45
+ // Check if this edge connects to our domain
46
+ if (edge.source === nodeId && edge.sourceHandle) {
47
+ // Extract service ID from sourceHandle (format: "serviceId-source")
48
+ const serviceId = edge.sourceHandle.replace('-source', '');
49
+ connectedServiceIds.add(serviceId);
50
+ }
51
+ if (edge.target === nodeId && edge.targetHandle) {
52
+ // Extract service ID from targetHandle (format: "serviceId-target")
53
+ const serviceId = edge.targetHandle.replace('-target', '');
54
+ connectedServiceIds.add(serviceId);
55
+ }
56
+ }
57
+ });
58
+
59
+ setHighlightedServices(connectedServiceIds);
60
+ },
61
+ });
62
+
63
+ return (
64
+ <ContextMenu.Root>
65
+ <ContextMenu.Trigger>
66
+ <div className="w-full rounded-lg border-2 border-yellow-400 bg-white shadow-lg">
67
+ <div className="bg-yellow-100 px-3 py-2 flex items-center space-x-2">
68
+ {Icon && <Icon className="w-4 h-4 text-yellow-700" />}
69
+ <div>
70
+ <span className="text-sm font-bold text-yellow-900">{name}</span>
71
+ <span className="text-xs text-yellow-700 ml-2">v{version}</span>
72
+ </div>
73
+ </div>
74
+ {mode === 'full' && services.length > 0 && (
75
+ <div>
76
+ {services.map((service: any, index: number) => {
77
+ const isHighlighted = highlightedServices.has(service.data.id);
78
+
79
+ return (
80
+ <ContextMenu.Root key={`${service.data.id}-${index}`}>
81
+ <ContextMenu.Trigger asChild>
82
+ <div
83
+ className={`relative flex items-center justify-between px-3 py-2 cursor-pointer ${index !== services.length - 1 ? 'border-b border-gray-300' : ''} ${isHighlighted ? 'bg-pink-100 border-pink-300' : ''}`}
84
+ >
85
+ <Handle
86
+ type="target"
87
+ position={Position.Left}
88
+ id={`${service.data.id}-target`}
89
+ className="!left-[-1px] !w-2 !h-2 !bg-gray-400 !border !border-gray-500 !rounded-full !z-10"
90
+ style={{ left: '-1px' }}
91
+ />
92
+ <Handle
93
+ type="source"
94
+ position={Position.Right}
95
+ id={`${service.data.id}-source`}
96
+ className="!right-[-1px] !w-2 !h-2 !bg-gray-400 !border !border-gray-500 !rounded-full !z-10"
97
+ style={{ right: '-1px' }}
98
+ />
99
+ <div className="flex items-center space-x-3">
100
+ <div className="flex items-center justify-center w-5 h-5 bg-pink-500 rounded">
101
+ {ServerIcon && <ServerIcon className="w-3 h-3 text-white" />}
102
+ </div>
103
+ <span className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</span>
104
+ </div>
105
+ <div className="flex items-center space-x-4 text-sm text-gray-600">
106
+ <span className="text-xs">v{service.data.version}</span>
107
+ </div>
108
+ </div>
109
+ </ContextMenu.Trigger>
110
+ <ContextMenu.Portal>
111
+ <ContextMenu.Content className="min-w-[220px] bg-white rounded-md p-1 shadow-md border border-gray-200">
112
+ <ContextMenu.Item
113
+ className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
114
+ onClick={() =>
115
+ (window.location.href = buildUrl(`/docs/services/${service.data.id}/${service.data.version}`))
116
+ }
117
+ >
118
+ View Service Documentation
119
+ </ContextMenu.Item>
120
+ <ContextMenu.Item
121
+ className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
122
+ onClick={() =>
123
+ (window.location.href = buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}`))
124
+ }
125
+ >
126
+ View Service Visualizer
127
+ </ContextMenu.Item>
128
+ </ContextMenu.Content>
129
+ </ContextMenu.Portal>
130
+ </ContextMenu.Root>
131
+ );
132
+ })}
133
+ </div>
134
+ )}
135
+ </div>
136
+ </ContextMenu.Trigger>
137
+ <ContextMenu.Portal>
138
+ <ContextMenu.Content className="min-w-[220px] bg-white rounded-md p-1 shadow-md border border-gray-200">
139
+ <ContextMenu.Item
140
+ className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
141
+ onClick={() => (window.location.href = buildUrl(`/docs/domains/${id}/${version}`))}
142
+ >
143
+ View Domain Documentation
144
+ </ContextMenu.Item>
145
+ <ContextMenu.Item
146
+ className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
147
+ onClick={() => (window.location.href = buildUrl(`/visualiser/domains/${id}/${version}`))}
148
+ >
149
+ View Domain Visualizer
150
+ </ContextMenu.Item>
151
+ </ContextMenu.Content>
152
+ </ContextMenu.Portal>
153
+ </ContextMenu.Root>
154
+ );
155
+ }
@@ -1,9 +1,42 @@
1
1
  import { useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
2
2
  import type { Node } from '@xyflow/react';
3
3
 
4
+ // Define interfaces for different node data structures
5
+ interface MessageData {
6
+ name: string;
7
+ version?: string;
8
+ }
9
+
10
+ interface ServiceData {
11
+ name: string;
12
+ version?: string;
13
+ }
14
+
15
+ interface DomainData {
16
+ name: string;
17
+ version?: string;
18
+ }
19
+
20
+ interface NodeDataContent extends Record<string, unknown> {
21
+ message?: {
22
+ data: MessageData;
23
+ };
24
+ service?: {
25
+ data: ServiceData;
26
+ };
27
+ domain?: {
28
+ data: DomainData;
29
+ };
30
+ name?: string;
31
+ version?: string;
32
+ }
33
+
34
+ // Extend the Node type with our custom data structure
35
+ type CustomNode = Node<NodeDataContent>;
36
+
4
37
  interface VisualiserSearchProps {
5
- nodes: Node[];
6
- onNodeSelect: (node: Node) => void;
38
+ nodes: CustomNode[];
39
+ onNodeSelect: (node: CustomNode) => void;
7
40
  onClear: () => void;
8
41
  onPaneClick?: () => void;
9
42
  }
@@ -15,7 +48,7 @@ export interface VisualiserSearchRef {
15
48
  const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
16
49
  ({ nodes, onNodeSelect, onClear, onPaneClick }, ref) => {
17
50
  const [searchQuery, setSearchQuery] = useState('');
18
- const [filteredSuggestions, setFilteredSuggestions] = useState<Node[]>([]);
51
+ const [filteredSuggestions, setFilteredSuggestions] = useState<CustomNode[]>([]);
19
52
  const [showSuggestions, setShowSuggestions] = useState(false);
20
53
  const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
21
54
  const searchInputRef = useRef<HTMLInputElement>(null);
@@ -34,11 +67,18 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
34
67
  [hideSuggestions]
35
68
  );
36
69
 
37
- const getNodeDisplayName = useCallback((node: Node) => {
38
- // @ts-ignore
39
- const name = node.data?.message?.data?.name || node.data?.service?.data?.name || node.data?.name || node.id;
40
- // @ts-ignore
41
- const version = node.data?.message?.data?.version || node.data?.service?.data?.version || node.data?.version;
70
+ const getNodeDisplayName = useCallback((node: CustomNode) => {
71
+ const name =
72
+ node.data?.message?.data?.name ||
73
+ node.data?.service?.data?.name ||
74
+ node.data?.domain?.data?.name ||
75
+ node.data?.name ||
76
+ node.id;
77
+ const version =
78
+ node.data?.message?.data?.version ||
79
+ node.data?.service?.data?.version ||
80
+ node.data?.domain?.data?.version ||
81
+ node.data?.version;
42
82
  return version ? `${name} (v${version})` : name;
43
83
  }, []);
44
84
 
@@ -50,6 +90,7 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
50
90
  commands: 'bg-blue-600 text-white',
51
91
  queries: 'bg-green-600 text-white',
52
92
  channels: 'bg-gray-600 text-white',
93
+ domains: 'bg-yellow-500 text-white',
53
94
  externalSystem: 'bg-pink-600 text-white',
54
95
  actor: 'bg-yellow-500 text-white',
55
96
  step: 'bg-gray-700 text-white',
@@ -90,7 +131,7 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
90
131
  }, [nodes, searchQuery]);
91
132
 
92
133
  const handleSuggestionClick = useCallback(
93
- (node: Node) => {
134
+ (node: CustomNode) => {
94
135
  setSearchQuery(getNodeDisplayName(node));
95
136
  setShowSuggestions(false);
96
137
  onNodeSelect(node);
@@ -4,7 +4,7 @@ import { buildUrl, buildUrlWithParams } from '@utils/url-builder';
4
4
  import CollapsibleGroup from './components/CollapsibleGroup';
5
5
  import MessageList from './components/MessageList';
6
6
  import SpecificationsList from './components/SpecificationList';
7
- import type { MessageItem, ServiceItem, ListViewSideBarProps } from './types';
7
+ import type { MessageItem, ServiceItem, ListViewSideBarProps, DomainItem, FlowItem, Resources } from './types';
8
8
  const STORAGE_KEY = 'EventCatalog:catalogSidebarCollapsedGroups';
9
9
  const DEBOUNCE_DELAY = 300; // 300ms debounce delay
10
10
 
@@ -228,7 +228,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
228
228
  }, [searchTerm]);
229
229
 
230
230
  // Filter data based on search term
231
- const filteredData = useMemo(() => {
231
+ const filteredData: Resources = useMemo(() => {
232
232
  if (!debouncedSearchTerm) return data;
233
233
 
234
234
  const filterItem = (item: { label: string; id?: string }) => {
@@ -245,6 +245,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
245
245
  };
246
246
 
247
247
  return {
248
+ 'context-map': data['context-map']?.filter(filterItem) || [],
248
249
  domains: data.domains?.filter(filterItem) || [],
249
250
  services:
250
251
  data.services
@@ -326,6 +327,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
326
327
 
327
328
  const hasNoResults =
328
329
  debouncedSearchTerm &&
330
+ !filteredData['context-map']?.length &&
329
331
  !filteredData.domains?.length &&
330
332
  !filteredData.services?.length &&
331
333
  !filteredData.flows?.length &&
@@ -345,9 +347,33 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
345
347
  <NoResultsFound searchTerm={debouncedSearchTerm} />
346
348
  ) : (
347
349
  <>
350
+ {/* Bounded Context Map (Visualiser only) */}
351
+ {filteredData['context-map'] && filteredData.domains && filteredData.domains.length > 0 && (
352
+ <div className="pt-0">
353
+ <ul className="space-y-1">
354
+ {filteredData['context-map'].map((item: any) => (
355
+ <li key={item.href}>
356
+ <a
357
+ href={item.href}
358
+ className={`flex items-center justify-between px-2 py-0.5 text-xs font-bold rounded-md ${
359
+ decodedCurrentPath === item.href ? 'bg-purple-100 text-purple-900' : 'hover:bg-purple-100'
360
+ }`}
361
+ >
362
+ <span className="truncate flex flex-col items-start">
363
+ <HighlightedText text={item.label} searchTerm={debouncedSearchTerm} />
364
+ <span className="text-[10px] text-gray-500 font-light">Explore integrations between domains</span>
365
+ </span>
366
+ <span className="text-blue-600 ml-2 text-[10px] font-medium bg-blue-50 px-2 py-0.5 rounded">DOMAINS</span>
367
+ </a>
368
+ </li>
369
+ ))}
370
+ </ul>
371
+ </div>
372
+ )}
373
+
348
374
  {/* Domains */}
349
375
  {filteredData['domains'] && (
350
- <div>
376
+ <div className={`${isVisualizer ? 'pt-4 pb-2' : 'p-0'}`}>
351
377
  <ul className="space-y-2">
352
378
  {filteredData['domains'].map((item: any) => (
353
379
  <li key={item.href} className="space-y-0" data-active={decodedCurrentPath === item.href}>
@@ -5,6 +5,7 @@ export interface MessageItem {
5
5
  id: string;
6
6
  direction: 'sends' | 'receives';
7
7
  type: 'command' | 'query' | 'event';
8
+ collection: string;
8
9
  data: {
9
10
  name: string;
10
11
  };
@@ -35,21 +36,28 @@ export interface ServiceItem {
35
36
  }[];
36
37
  }
37
38
 
38
- interface DomainItem {
39
+ export interface DomainItem {
39
40
  href: string;
40
41
  label: string;
41
42
  id: string;
42
43
  name: string;
43
44
  services: any[];
44
45
  domains: any[];
46
+ entities: EntityItem[];
45
47
  }
46
48
 
47
- interface FlowItem {
49
+ export interface FlowItem {
48
50
  href: string;
49
51
  label: string;
50
52
  }
51
53
 
52
- interface Resources {
54
+ export interface Resources {
55
+ 'context-map'?: Array<{
56
+ href: string;
57
+ label: string;
58
+ id: string;
59
+ name: string;
60
+ }>;
53
61
  domains?: DomainItem[];
54
62
  services?: ServiceItem[];
55
63
  flows?: FlowItem[];
@@ -107,8 +107,12 @@ export async function getResourcesForNavigation({ currentPath }: { currentPath:
107
107
  const sideNav = {
108
108
  ...(currentPath.includes('visualiser')
109
109
  ? {
110
- 'bounded context map': [
111
- { label: 'Domain map', href: buildUrl('/visualiser/context-map'), collection: 'bounded-context-map' },
110
+ 'context-map': [
111
+ {
112
+ label: 'Domain Integration Map',
113
+ href: buildUrl('/visualiser/domain-integrations'),
114
+ collection: 'domain-integrations',
115
+ },
112
116
  ],
113
117
  }
114
118
  : {}),
@@ -0,0 +1,31 @@
1
+ ---
2
+ import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro';
3
+ import VisualiserLayout from '@layouts/VisualiserLayout.astro';
4
+ import { buildUrl } from '@utils/url-builder';
5
+ import { ClientRouter } from 'astro:transitions';
6
+ ---
7
+
8
+ <VisualiserLayout title={`Visualiser | Domain Architecture`} description="High-level view of all domains and their relationships">
9
+ <div class="bg-gray-100/50 m-4">
10
+ <div
11
+ class="h-[calc(100vh-130px)] w-full relative border border-gray-200"
12
+ id="domains-canvas-portal"
13
+ transition:animate="fade"
14
+ >
15
+ </div>
16
+ <NodeGraph
17
+ id="domains-canvas"
18
+ collection="domains-canvas"
19
+ title="Domain Integration Map"
20
+ mode="full"
21
+ linkTo="visualiser"
22
+ version="1.0.0"
23
+ linksToVisualiser={false}
24
+ href={{
25
+ label: 'View Documentation',
26
+ url: buildUrl('/docs'),
27
+ }}
28
+ />
29
+ </div>
30
+ <ClientRouter />
31
+ </VisualiserLayout>
@@ -0,0 +1,251 @@
1
+ import { getCollection, type CollectionEntry } from 'astro:content';
2
+ import dagre from 'dagre';
3
+ import { generateIdForNode, createDagreGraph, calculatedNodes, createEdge } from '@utils/node-graphs/utils/utils';
4
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
5
+ import type { Node, Edge } from '@xyflow/react';
6
+ import { getDomains } from '@utils/collections/domains';
7
+
8
+ interface DomainCanvasData {
9
+ domainNodes: Node[];
10
+ messageNodes: Node[];
11
+ edges: Edge[];
12
+ }
13
+
14
+ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
15
+ let domains = await getDomains({ getAllVersions: false });
16
+
17
+ // only interested in domains that are not parent domains (e.g domains that have subdoamins we dont want to display them here)
18
+ domains = domains.filter((domain) => !domain.data.domains?.length);
19
+
20
+ const domainNodes: Node[] = [];
21
+ const messageNodes: Node[] = [];
22
+ const edges: Edge[] = [];
23
+
24
+ // Create dagre graph for layout
25
+ const dagreGraph = createDagreGraph({ ranksep: 400, nodesep: 200 });
26
+
27
+ // Create a map to store domain data for calculating relationships
28
+ const domainDataMap = new Map();
29
+
30
+ for (let index = 0; index < domains.length; index++) {
31
+ const domain = domains[index];
32
+ const domainNodeId = generateIdForNode(domain);
33
+ const domainServices = domain.data.services as unknown as CollectionEntry<'services'>[];
34
+
35
+ // Count total messages for this domain
36
+ let totalMessages = 0;
37
+ for (const service of domainServices) {
38
+ const receives = service.data.receives ?? [];
39
+ const sends = service.data.sends ?? [];
40
+ totalMessages += receives.length + sends.length;
41
+ }
42
+
43
+ domainDataMap.set(domainNodeId, {
44
+ domain,
45
+ services: domainServices,
46
+ servicesCount: domainServices.length,
47
+ messagesCount: totalMessages,
48
+ });
49
+
50
+ // Domain Overview Node (position will be calculated by dagre)
51
+ domainNodes.push({
52
+ id: domainNodeId,
53
+ type: 'domains',
54
+ position: { x: 0, y: 0 }, // Temporary position, will be calculated by dagre
55
+ data: {
56
+ mode: 'full',
57
+ domain: {
58
+ ...domain,
59
+ data: {
60
+ ...domain.data,
61
+ services: domainServices,
62
+ },
63
+ },
64
+ servicesCount: domainServices.length,
65
+ messagesCount: totalMessages,
66
+ },
67
+ sourcePosition: 'right',
68
+ targetPosition: 'left',
69
+ } as Node);
70
+ }
71
+
72
+ // Get all messages for version resolution
73
+ const allMessages = await getCollection('events')
74
+ .then((events) => Promise.all([events, getCollection('commands'), getCollection('queries')]))
75
+ .then(([events, commands, queries]) => [...events, ...commands, ...queries]);
76
+
77
+ // Map to track unique messages and their publishers/consumers across domains
78
+ const messageRelationships = new Map<
79
+ string,
80
+ {
81
+ message: any;
82
+ publishers: Array<{ domainId: string; service: any }>;
83
+ consumers: Array<{ domainId: string; service: any }>;
84
+ }
85
+ >();
86
+
87
+ // Find all message relationships across domains
88
+ domainDataMap.forEach((domainData, domainId) => {
89
+ domainData.services.forEach((service: any) => {
90
+ // Track messages this service sends
91
+ const sendsRaw = service.data.sends ?? [];
92
+ const sendsHydrated = sendsRaw
93
+ .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
94
+ .flat()
95
+ .filter((e: any) => e !== undefined);
96
+
97
+ sendsHydrated.forEach((sentMessage: any) => {
98
+ const messageKey = `${sentMessage.data.id}-${sentMessage.data.version}`;
99
+
100
+ if (!messageRelationships.has(messageKey)) {
101
+ messageRelationships.set(messageKey, {
102
+ message: sentMessage.data,
103
+ publishers: [],
104
+ consumers: [],
105
+ });
106
+ }
107
+
108
+ const relationship = messageRelationships.get(messageKey)!;
109
+ // Add publisher if not already added
110
+ if (!relationship.publishers.some((p) => p.domainId === domainId && p.service.data.id === service.data.id)) {
111
+ relationship.publishers.push({ domainId, service });
112
+ }
113
+ });
114
+
115
+ // Track messages this service receives
116
+ const receivesRaw = service.data.receives ?? [];
117
+ const receivesHydrated = receivesRaw
118
+ .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
119
+ .flat()
120
+ .filter((e: any) => e !== undefined);
121
+
122
+ receivesHydrated.forEach((receivedMessage: any) => {
123
+ const messageKey = `${receivedMessage.data.id}-${receivedMessage.data.version}`;
124
+
125
+ if (!messageRelationships.has(messageKey)) {
126
+ messageRelationships.set(messageKey, {
127
+ message: receivedMessage.data,
128
+ publishers: [],
129
+ consumers: [],
130
+ });
131
+ }
132
+
133
+ const relationship = messageRelationships.get(messageKey)!;
134
+ // Add consumer if not already added
135
+ if (!relationship.consumers.some((c) => c.domainId === domainId && c.service.data.id === service.data.id)) {
136
+ relationship.consumers.push({ domainId, service });
137
+ }
138
+ });
139
+ });
140
+ });
141
+
142
+ // Create message nodes and edges for cross-domain relationships
143
+
144
+ // Only create message nodes for messages that cross domain boundaries
145
+ messageRelationships.forEach(({ message, publishers, consumers }, messageKey) => {
146
+ // Check if this message crosses domain boundaries
147
+ const publisherDomains = new Set(publishers.map((p) => p.domainId));
148
+ const consumerDomains = new Set(consumers.map((c) => c.domainId));
149
+
150
+ // Only create a message node if it connects different domains
151
+ const crossesDomainBoundary = [...publisherDomains].some((pubDomain) =>
152
+ [...consumerDomains].some((conDomain) => pubDomain !== conDomain)
153
+ );
154
+
155
+ if (crossesDomainBoundary) {
156
+ // Find the actual message object
157
+ const messageObject = allMessages.find((m) => m.data.id === message.id && m.data.version === message.version);
158
+
159
+ if (messageObject) {
160
+ const messageNodeId = `message-${messageKey}`;
161
+
162
+ // Create a single message node for this unique message
163
+ messageNodes.push({
164
+ id: messageNodeId,
165
+ type: messageObject.collection, // events, commands, or queries
166
+ position: { x: 0, y: 0 }, // Temporary position, will be calculated by dagre
167
+ data: {
168
+ mode: 'simple',
169
+ message: messageObject,
170
+ },
171
+ sourcePosition: 'right',
172
+ targetPosition: 'left',
173
+ } as Node);
174
+
175
+ // Create edges from all publishers to the message node
176
+ publishers.forEach(({ domainId, service }) => {
177
+ // Only create edge if there's a consumer in a different domain
178
+ const hasExternalConsumer = consumers.some((c) => c.domainId !== domainId);
179
+
180
+ if (hasExternalConsumer) {
181
+ edges.push(
182
+ createEdge({
183
+ id: `edge-${domainId}-${service.data.id}-${messageNodeId}`,
184
+ source: domainId,
185
+ sourceHandle: `${service.data.id}-source`,
186
+ target: messageNodeId,
187
+ type: 'animated',
188
+ animated: true,
189
+ label: 'publishes',
190
+ data: {
191
+ message: messageObject,
192
+ type: 'domain-to-message',
193
+ publisherService: service,
194
+ },
195
+ })
196
+ );
197
+ }
198
+ });
199
+
200
+ // Create edges from the message node to all consumers
201
+ consumers.forEach(({ domainId, service }) => {
202
+ // Only create edge if there's a publisher in a different domain
203
+ const hasExternalPublisher = publishers.some((p) => p.domainId !== domainId);
204
+
205
+ if (hasExternalPublisher) {
206
+ edges.push(
207
+ createEdge({
208
+ id: `edge-${messageNodeId}-${domainId}-${service.data.id}`,
209
+ source: messageNodeId,
210
+ target: domainId,
211
+ targetHandle: `${service.data.id}-target`,
212
+ type: 'animated',
213
+ animated: true,
214
+ label: 'consumed by',
215
+ data: {
216
+ message: messageObject,
217
+ type: 'message-to-domain',
218
+ consumerService: service,
219
+ },
220
+ })
221
+ );
222
+ }
223
+ });
224
+ }
225
+ }
226
+ });
227
+
228
+ // Add all nodes to dagre graph for layout calculation
229
+ const allNodes = [...domainNodes, ...messageNodes];
230
+
231
+ allNodes.forEach((node) => {
232
+ dagreGraph.setNode(node.id, { width: 250, height: 120 });
233
+ });
234
+
235
+ edges.forEach((edge) => {
236
+ dagreGraph.setEdge(edge.source, edge.target);
237
+ });
238
+
239
+ // Calculate layout using dagre
240
+ dagre.layout(dagreGraph);
241
+
242
+ // Apply calculated positions to nodes
243
+ const layoutedDomainNodes = calculatedNodes(dagreGraph, domainNodes);
244
+ const layoutedMessageNodes = calculatedNodes(dagreGraph, messageNodes);
245
+
246
+ return {
247
+ domainNodes: layoutedDomainNodes,
248
+ messageNodes: layoutedMessageNodes,
249
+ edges,
250
+ };
251
+ };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "2.44.5",
9
+ "version": "2.45.1",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },