@eventcatalog/core 3.7.2 → 3.8.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.
Files changed (66) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-M7EPRGHR.js → chunk-4BEERWPE.js} +1 -1
  6. package/dist/{chunk-GQZVIS3Z.js → chunk-NV7H3356.js} +1 -1
  7. package/dist/{chunk-7CTNGTBB.js → chunk-POQENB7N.js} +1 -1
  8. package/dist/{chunk-WAX3S32H.js → chunk-SL2YYN6D.js} +1 -1
  9. package/dist/{chunk-O6SRHGZ7.js → chunk-ZBC6HM3V.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +13 -1
  19. package/eventcatalog/src/components/Grids/DomainGrid.tsx +109 -6
  20. package/eventcatalog/src/components/Grids/utils.tsx +10 -1
  21. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -0
  22. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +4 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +132 -0
  24. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +29 -2
  25. package/eventcatalog/src/components/SchemaExplorer/types.ts +5 -1
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +3 -0
  27. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +1 -0
  28. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +23 -1
  29. package/eventcatalog/src/components/Tables/Discover/columns.tsx +62 -0
  30. package/eventcatalog/src/content.config.ts +34 -0
  31. package/eventcatalog/src/enterprise/ai/chat-api.ts +26 -0
  32. package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +1 -1
  33. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +169 -2
  34. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -1
  35. package/eventcatalog/src/pages/discover/[type]/index.astro +57 -1
  36. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
  37. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -1
  38. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +27 -3
  39. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +74 -25
  40. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +55 -1
  41. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +10 -1
  42. package/eventcatalog/src/stores/sidebar-store/builders/container.ts +23 -16
  43. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +130 -0
  44. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +11 -0
  45. package/eventcatalog/src/stores/sidebar-store/state.ts +68 -13
  46. package/eventcatalog/src/styles/theme.css +4 -0
  47. package/eventcatalog/src/styles/themes/forest.css +4 -0
  48. package/eventcatalog/src/styles/themes/ocean.css +4 -0
  49. package/eventcatalog/src/styles/themes/sapphire.css +4 -0
  50. package/eventcatalog/src/styles/themes/sunset.css +4 -0
  51. package/eventcatalog/src/types/index.ts +4 -2
  52. package/eventcatalog/src/utils/collections/commands.ts +11 -29
  53. package/eventcatalog/src/utils/collections/containers.ts +25 -1
  54. package/eventcatalog/src/utils/collections/data-products.ts +85 -0
  55. package/eventcatalog/src/utils/collections/domains.ts +28 -10
  56. package/eventcatalog/src/utils/collections/events.ts +11 -29
  57. package/eventcatalog/src/utils/collections/icons.ts +5 -0
  58. package/eventcatalog/src/utils/collections/messages.ts +68 -0
  59. package/eventcatalog/src/utils/collections/queries.ts +11 -29
  60. package/eventcatalog/src/utils/collections/util.ts +11 -2
  61. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +91 -3
  62. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +225 -0
  63. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +28 -2
  64. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +74 -20
  65. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  66. package/package.json +2 -2
@@ -3,6 +3,7 @@ import { getCommands } from '@utils/collections/commands';
3
3
  import { getEvents } from '@utils/collections/events';
4
4
  import { getQueries } from './queries';
5
5
  import type { CollectionEntry } from 'astro:content';
6
+ import { satisfies } from './util';
6
7
  export { getCommands } from '@utils/collections/commands';
7
8
  export { getEvents } from '@utils/collections/events';
8
9
 
@@ -11,6 +12,69 @@ interface Props {
11
12
  hydrateServices?: boolean;
12
13
  }
13
14
 
15
+ interface HydrateProducersAndConsumersProps {
16
+ message: {
17
+ data: {
18
+ id: string;
19
+ version: string;
20
+ latestVersion?: string;
21
+ };
22
+ };
23
+ services: CollectionEntry<'services'>[];
24
+ dataProducts: CollectionEntry<'data-products'>[];
25
+ hydrate?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Hydrates producers and consumers for a message (event, command, or query).
30
+ * Finds services and data products that produce or consume the given message.
31
+ */
32
+ export const hydrateProducersAndConsumers = ({
33
+ message,
34
+ services = [],
35
+ dataProducts = [],
36
+ hydrate = true,
37
+ }: HydrateProducersAndConsumersProps) => {
38
+ const { id: messageId, version: messageVersion, latestVersion = messageVersion } = message.data;
39
+
40
+ const matchesVersion = (pointerVersion: string | undefined) => {
41
+ if (pointerVersion === 'latest' || pointerVersion === undefined) {
42
+ return messageVersion === latestVersion;
43
+ }
44
+ return satisfies(messageVersion, pointerVersion);
45
+ };
46
+
47
+ const toResult = <T extends CollectionEntry<'services'> | CollectionEntry<'data-products'>>(resource: T) => {
48
+ if (!hydrate) return { id: resource.data.id, version: resource.data.version };
49
+ return resource;
50
+ };
51
+
52
+ // Services that send this message (producers)
53
+ const serviceProducers = services
54
+ .filter((s) => s.data.sends?.some((p) => p.id === messageId && matchesVersion(p.version)))
55
+ .map(toResult);
56
+
57
+ // Services that receive this message (consumers)
58
+ const serviceConsumers = services
59
+ .filter((s) => s.data.receives?.some((p) => p.id === messageId && matchesVersion(p.version)))
60
+ .map(toResult);
61
+
62
+ // Data products that output this message (producers)
63
+ const dataProductProducers = dataProducts
64
+ .filter((dp) => dp.data.outputs?.some((p) => p.id === messageId && matchesVersion(p.version)))
65
+ .map(toResult);
66
+
67
+ // Data products that input this message (consumers)
68
+ const dataProductConsumers = dataProducts
69
+ .filter((dp) => dp.data.inputs?.some((p) => p.id === messageId && matchesVersion(p.version)))
70
+ .map(toResult);
71
+
72
+ return {
73
+ producers: [...serviceProducers, ...dataProductProducers],
74
+ consumers: [...serviceConsumers, ...dataProductConsumers],
75
+ };
76
+ };
77
+
14
78
  type Messages = {
15
79
  commands: CollectionEntry<'commands'>[];
16
80
  events: CollectionEntry<'events'>[];
@@ -40,3 +104,7 @@ export const getMessages = async ({ getAllVersions = true, hydrateServices = tru
40
104
  queries,
41
105
  };
42
106
  };
107
+
108
+ export const isCollectionAMessage = (collection: string): boolean => {
109
+ return ['events', 'commands', 'queries'].includes(collection);
110
+ };
@@ -2,7 +2,8 @@ import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import path from 'path';
4
4
  import utils from '@eventcatalog/sdk';
5
- import { createVersionedMap, satisfies } from './util';
5
+ import { createVersionedMap } from './util';
6
+ import { hydrateProducersAndConsumers } from './messages';
6
7
 
7
8
  const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
8
9
  const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
@@ -34,10 +35,11 @@ export const getQueries = async ({ getAllVersions = true, hydrateServices = true
34
35
  }
35
36
 
36
37
  // 1. Fetch collections in parallel
37
- const [allQueries, allServices, allChannels] = await Promise.all([
38
+ const [allQueries, allServices, allChannels, allDataProducts] = await Promise.all([
38
39
  getCollection('queries'),
39
40
  getCollection('services'),
40
41
  getCollection('channels'),
42
+ getCollection('data-products'),
41
43
  ]);
42
44
 
43
45
  // 2. Build optimized maps
@@ -60,33 +62,13 @@ export const getQueries = async ({ getAllVersions = true, hydrateServices = true
60
62
  const latestVersion = queryVersions[0]?.data.version || query.data.version;
61
63
  const versions = queryVersions.map((e) => e.data.version);
62
64
 
63
- // Find Producers (Services that send this query)
64
- const producers = allServices
65
- .filter((service) =>
66
- service.data.sends?.some((item) => {
67
- if (item.id !== query.data.id) return false;
68
- if (item.version === 'latest' || item.version === undefined) return query.data.version === latestVersion;
69
- return satisfies(query.data.version, item.version);
70
- })
71
- )
72
- .map((service) => {
73
- if (!hydrateServices) return { id: service.data.id, version: service.data.version };
74
- return service;
75
- });
76
-
77
- // Find Consumers (Services that receive this query)
78
- const consumers = allServices
79
- .filter((service) =>
80
- service.data.receives?.some((item) => {
81
- if (item.id !== query.data.id) return false;
82
- if (item.version === 'latest' || item.version === undefined) return query.data.version === latestVersion;
83
- return satisfies(query.data.version, item.version);
84
- })
85
- )
86
- .map((service) => {
87
- if (!hydrateServices) return { id: service.data.id, version: service.data.version };
88
- return service;
89
- });
65
+ // Find producers and consumers (services + data products)
66
+ const { producers, consumers } = hydrateProducersAndConsumers({
67
+ message: { data: { ...query.data, latestVersion } },
68
+ services: allServices,
69
+ dataProducts: allDataProducts,
70
+ hydrate: hydrateServices,
71
+ });
90
72
 
91
73
  // Find Channels
92
74
  const messageChannels = query.data.channels || [];
@@ -158,8 +158,8 @@ export const getItemsFromCollectionByIdAndSemverOrLatest = <T extends { data: {
158
158
  };
159
159
 
160
160
  export const findMatchingNodes = (
161
- nodesA: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers'>[],
162
- nodesB: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers'>[]
161
+ nodesA: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers' | 'data-products'>[],
162
+ nodesB: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers' | 'data-products'>[]
163
163
  ) => {
164
164
  // Track messages that are both sent and received
165
165
  return nodesA.filter((nodeA) => {
@@ -182,6 +182,7 @@ export const resourceToCollectionMap = {
182
182
  container: 'containers',
183
183
  entity: 'entities',
184
184
  diagram: 'diagrams',
185
+ 'data-product': 'data-products',
185
186
  } as const;
186
187
 
187
188
  export const collectionToResourceMap = {
@@ -197,6 +198,7 @@ export const collectionToResourceMap = {
197
198
  containers: 'container',
198
199
  entities: 'entity',
199
200
  diagrams: 'diagram',
201
+ 'data-products': 'data-product',
200
202
  } as const;
201
203
 
202
204
  export const getDeprecatedDetails = (item: CollectionEntry<CollectionTypes>) => {
@@ -261,6 +263,13 @@ export const createVersionedMap = <T extends { data: { id: string; version?: str
261
263
  return map;
262
264
  };
263
265
 
266
+ // Merge as many given maps as you want
267
+ export const mergeMaps = <T>(...maps: Map<string, T[]>[]): Map<string, T[]> => {
268
+ return maps.reduce((acc, map) => {
269
+ return new Map([...acc, ...map]);
270
+ }, new Map<string, T[]>());
271
+ };
272
+
264
273
  /**
265
274
  * Fast lookup helper.
266
275
  * If version is provided, find it. If not, return the first (latest) item.
@@ -37,9 +37,14 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
37
37
 
38
38
  const servicesThatWriteToContainer = (container.data.servicesThatWriteToContainer as CollectionEntry<'services'>[]) || [];
39
39
  const servicesThatReadFromContainer = (container.data.servicesThatReadFromContainer as CollectionEntry<'services'>[]) || [];
40
+ const dataProductsThatWriteToContainer =
41
+ (container.data.dataProductsThatWriteToContainer as CollectionEntry<'data-products'>[]) || [];
42
+ const dataProductsThatReadFromContainer =
43
+ (container.data.dataProductsThatReadFromContainer as CollectionEntry<'data-products'>[]) || [];
40
44
 
41
- // Track nodes that are bth sent and received
45
+ // Track nodes that are both sent and received
42
46
  const bothSentAndReceived = findMatchingNodes(servicesThatWriteToContainer, servicesThatReadFromContainer);
47
+ const dataProductsBothSentAndReceived = findMatchingNodes(dataProductsThatWriteToContainer, dataProductsThatReadFromContainer);
43
48
 
44
49
  servicesThatWriteToContainer.forEach((service) => {
45
50
  nodes.push({
@@ -67,7 +72,34 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
67
72
  }
68
73
  });
69
74
 
70
- // The message itself
75
+ // Data products that write to the container
76
+ dataProductsThatWriteToContainer.forEach((dataProduct) => {
77
+ nodes.push({
78
+ id: generateIdForNode(dataProduct),
79
+ type: 'data-products',
80
+ sourcePosition: 'right',
81
+ targetPosition: 'left',
82
+ data: { mode, dataProduct: { ...dataProduct.data } },
83
+ position: { x: 250, y: 0 },
84
+ });
85
+
86
+ if (!dataProductsBothSentAndReceived.includes(dataProduct)) {
87
+ edges.push({
88
+ id: generatedIdForEdge(dataProduct, container),
89
+ source: generateIdForNode(dataProduct),
90
+ target: generateIdForNode(container),
91
+ label: 'writes to',
92
+ data: { dataProduct },
93
+ animated: false,
94
+ type: 'default',
95
+ style: {
96
+ strokeWidth: 1,
97
+ },
98
+ });
99
+ }
100
+ });
101
+
102
+ // The container itself
71
103
  nodes.push({
72
104
  id: generateIdForNode(container),
73
105
  sourcePosition: 'right',
@@ -114,7 +146,38 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
114
146
  }
115
147
  });
116
148
 
117
- // Handle messages that are both sent and received
149
+ // Data products that read from the container
150
+ dataProductsThatReadFromContainer.forEach((dataProduct) => {
151
+ nodes.push({
152
+ id: generateIdForNode(dataProduct),
153
+ sourcePosition: 'left',
154
+ targetPosition: 'right',
155
+ data: { title: dataProduct?.data.id, mode, dataProduct: { ...dataProduct.data } },
156
+ position: { x: 0, y: 0 },
157
+ type: 'data-products',
158
+ });
159
+
160
+ if (!dataProductsBothSentAndReceived.includes(dataProduct)) {
161
+ edges.push(
162
+ createEdge({
163
+ id: generatedIdForEdge(dataProduct, container),
164
+ source: generateIdForNode(container),
165
+ target: generateIdForNode(dataProduct),
166
+ label: `reads from \n (${container.data.technology})`,
167
+ data: { dataProduct },
168
+ type: 'multiline',
169
+ markerStart: {
170
+ type: MarkerType.ArrowClosed,
171
+ width: 40,
172
+ height: 40,
173
+ },
174
+ markerEnd: undefined,
175
+ })
176
+ );
177
+ }
178
+ });
179
+
180
+ // Handle services that are both sent and received
118
181
  bothSentAndReceived.forEach((_service) => {
119
182
  if (container) {
120
183
  edges.push(
@@ -139,6 +202,31 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
139
202
  }
140
203
  });
141
204
 
205
+ // Handle data products that both read and write
206
+ dataProductsBothSentAndReceived.forEach((_dataProduct) => {
207
+ if (container) {
208
+ edges.push(
209
+ createEdge({
210
+ id: generatedIdForEdge(container, _dataProduct) + '-both',
211
+ source: generateIdForNode(_dataProduct),
212
+ target: generateIdForNode(container),
213
+ label: `read and writes to \n (${container.data.technology})`,
214
+ type: 'multiline',
215
+ markerStart: {
216
+ type: MarkerType.ArrowClosed,
217
+ width: 40,
218
+ height: 40,
219
+ },
220
+ markerEnd: {
221
+ type: MarkerType.ArrowClosed,
222
+ width: 40,
223
+ height: 40,
224
+ },
225
+ })
226
+ );
227
+ }
228
+ });
229
+
142
230
  nodes.forEach((node: any) => {
143
231
  flow.setNode(node.id, { width: 150, height: 100 });
144
232
  });
@@ -0,0 +1,225 @@
1
+ import { getCollection, type CollectionEntry } from 'astro:content';
2
+ import dagre from 'dagre';
3
+ import {
4
+ createDagreGraph,
5
+ generateIdForNode,
6
+ generatedIdForEdge,
7
+ calculatedNodes,
8
+ createEdge,
9
+ getColorFromString,
10
+ } from '@utils/node-graphs/utils/utils';
11
+
12
+ import { findInMap, createVersionedMap, mergeMaps, collectionToResourceMap } from '@utils/collections/util';
13
+ import { MarkerType } from '@xyflow/react';
14
+ import { getMessages, isCollectionAMessage } from '@utils/collections/messages';
15
+ import { getProducersOfMessage } from '@utils/collections/services';
16
+ import type { CollectionMessageTypes } from '@types';
17
+ import { getNodesAndEdgesForProducedMessage } from './message-node-graph';
18
+
19
+ type DagreGraph = any;
20
+
21
+ interface Props {
22
+ id: string;
23
+ version: string;
24
+ defaultFlow?: DagreGraph;
25
+ mode?: 'simple' | 'full';
26
+ }
27
+
28
+ const getNodePropertyFromCollectionType = (type: string) => {
29
+ if (isCollectionAMessage(type)) return 'message';
30
+ if (type === 'containers') return 'data';
31
+ return collectionToResourceMap[type as keyof typeof collectionToResourceMap];
32
+ };
33
+
34
+ export const getNodesAndEdges = async ({ id, defaultFlow, version, mode = 'simple' }: Props) => {
35
+ const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 });
36
+ let nodes = [] as any,
37
+ edges = [] as any;
38
+
39
+ const [dataProducts, containers, services, events, queries, commands, channels] = await Promise.all([
40
+ getCollection('data-products'),
41
+ getCollection('containers'),
42
+ getCollection('services'),
43
+ getCollection('events'),
44
+ getCollection('queries'),
45
+ getCollection('commands'),
46
+ getCollection('channels'),
47
+ ]);
48
+
49
+ const dataProduct = dataProducts.find((dp) => dp.data.id === id && dp.data.version === version);
50
+
51
+ // Nothing found...
52
+ if (!dataProduct) {
53
+ return {
54
+ nodes: [],
55
+ edges: [],
56
+ };
57
+ }
58
+
59
+ // Build maps for O(1) lookups
60
+ const messages = [...events, ...commands, ...queries];
61
+
62
+ const messageMap = createVersionedMap(messages);
63
+ const containerMap = createVersionedMap(containers);
64
+ const serviceMap = createVersionedMap(services);
65
+ const channelMap = createVersionedMap(channels);
66
+
67
+ const inputsRaw = dataProduct?.data.inputs || [];
68
+ const outputsRaw = dataProduct?.data.outputs || [];
69
+
70
+ const resourceMap = mergeMaps<
71
+ | CollectionEntry<CollectionMessageTypes>
72
+ | CollectionEntry<'services'>
73
+ | CollectionEntry<'containers'>
74
+ | CollectionEntry<'channels'>
75
+ >(messageMap, serviceMap, containerMap, channelMap);
76
+
77
+ // Process inputs - messages, containers, services, channels (etc)
78
+ inputsRaw.forEach((inputConfig) => {
79
+ let inputResource = findInMap(resourceMap, inputConfig.id, inputConfig.version) as
80
+ | CollectionEntry<CollectionMessageTypes>
81
+ | CollectionEntry<'services'>
82
+ | CollectionEntry<'containers'>
83
+ | CollectionEntry<'channels'>;
84
+
85
+ const existingNode = nodes.find((n: any) => n.id === generateIdForNode(inputResource));
86
+
87
+ if (!existingNode) {
88
+ const nodeDataKey = getNodePropertyFromCollectionType(inputResource?.collection);
89
+
90
+ nodes.push({
91
+ id: generateIdForNode(inputResource),
92
+ sourcePosition: 'right',
93
+ targetPosition: 'left',
94
+ data: { mode, [nodeDataKey]: { ...inputResource?.data } },
95
+ type: (inputResource?.collection as any) === 'containers' ? 'data' : inputResource?.collection,
96
+ });
97
+ }
98
+
99
+ // If collection is a message we render the producers of the message
100
+ if (isCollectionAMessage(inputResource?.collection)) {
101
+ const producersOfMessage = getProducersOfMessage(
102
+ services as CollectionEntry<'services'>[],
103
+ inputResource as CollectionEntry<CollectionMessageTypes>
104
+ ) as CollectionEntry<'services'>[];
105
+ for (const producer of producersOfMessage) {
106
+ const { nodes: producerNodes, edges: producerEdges } = getNodesAndEdgesForProducedMessage({
107
+ message: inputResource as CollectionEntry<CollectionMessageTypes>,
108
+ // We dont render any other services that consume this event for now
109
+ services: [],
110
+ // We dont render channels on this view for now...
111
+ channels: [],
112
+ currentNodes: nodes,
113
+ currentEdges: edges,
114
+ source: producer,
115
+ mode,
116
+ });
117
+
118
+ nodes.push(...producerNodes);
119
+ edges.push(...producerEdges);
120
+ }
121
+ }
122
+
123
+ // Add edge from resource to data product
124
+ edges.push(
125
+ createEdge({
126
+ id: generatedIdForEdge(inputResource, dataProduct),
127
+ source: generateIdForNode(inputResource),
128
+ target: generateIdForNode(dataProduct),
129
+ label: 'input',
130
+ type: 'animated',
131
+ data: {
132
+ customColor: getColorFromString(inputResource.data.id),
133
+ rootSourceAndTarget: { source: inputResource, target: dataProduct },
134
+ },
135
+ markerEnd: {
136
+ type: MarkerType.ArrowClosed,
137
+ color: '#666',
138
+ width: 40,
139
+ height: 40,
140
+ },
141
+ })
142
+ );
143
+ });
144
+
145
+ // The data product itself
146
+ nodes.push({
147
+ id: generateIdForNode(dataProduct),
148
+ sourcePosition: 'right',
149
+ targetPosition: 'left',
150
+ data: { mode, dataProduct: { ...dataProduct.data } },
151
+ type: 'data-products',
152
+ });
153
+
154
+ // Process outputs - messages, services, containers, channels that the data product produces
155
+ outputsRaw.forEach((outputConfig) => {
156
+ // Find the output resource (can be message, service, container, or channel)
157
+ const outputResource = findInMap(resourceMap, outputConfig.id, outputConfig.version) as
158
+ | CollectionEntry<CollectionMessageTypes>
159
+ | CollectionEntry<'services'>
160
+ | CollectionEntry<'containers'>
161
+ | CollectionEntry<'channels'>;
162
+
163
+ if (!outputResource) return;
164
+
165
+ // Add the node if it doesn't exist
166
+ const existingNode = nodes.find((n: any) => n.id === generateIdForNode(outputResource));
167
+ if (!existingNode) {
168
+ const nodeDataKey = getNodePropertyFromCollectionType(outputResource?.collection);
169
+
170
+ nodes.push({
171
+ id: generateIdForNode(outputResource),
172
+ sourcePosition: 'right',
173
+ targetPosition: 'left',
174
+ data: { mode, [nodeDataKey]: { ...outputResource?.data } },
175
+ type: (outputResource?.collection as any) === 'containers' ? 'data' : outputResource?.collection,
176
+ });
177
+ }
178
+
179
+ // Add edge from data product to the output resource
180
+ edges.push(
181
+ createEdge({
182
+ id: generatedIdForEdge(dataProduct, outputResource),
183
+ source: generateIdForNode(dataProduct),
184
+ target: generateIdForNode(outputResource),
185
+ label: 'output',
186
+ type: 'animated',
187
+ data: {
188
+ customColor: getColorFromString(outputResource.data.id),
189
+ rootSourceAndTarget: { source: dataProduct, target: outputResource },
190
+ },
191
+ markerEnd: {
192
+ type: MarkerType.ArrowClosed,
193
+ color: '#666',
194
+ width: 40,
195
+ height: 40,
196
+ },
197
+ })
198
+ );
199
+ });
200
+
201
+ nodes.forEach((node: any) => {
202
+ flow.setNode(node.id, { width: 150, height: 100 });
203
+ });
204
+
205
+ edges.forEach((edge: any) => {
206
+ flow.setEdge(edge.source, edge.target);
207
+ });
208
+
209
+ // Render the diagram in memory getting the X and Y
210
+ dagre.layout(flow);
211
+
212
+ // Find any duplicated edges, and merge them into one edge
213
+ const uniqueEdges = edges.reduce((acc: any[], edge: any) => {
214
+ const existingEdge = acc.find((e: any) => e.id === edge.id);
215
+ if (!existingEdge) {
216
+ acc.push(edge);
217
+ }
218
+ return acc;
219
+ }, []);
220
+
221
+ return {
222
+ nodes: calculatedNodes(flow, nodes),
223
+ edges: uniqueEdges,
224
+ };
225
+ };
@@ -8,6 +8,7 @@ import {
8
8
  createEdge,
9
9
  } from '@utils/node-graphs/utils/utils';
10
10
  import { getNodesAndEdges as getServicesNodeAndEdges } from './services-node-graph';
11
+ import { getNodesAndEdges as getDataProductsNodeAndEdges } from './data-products-node-graph';
11
12
  import merge from 'lodash.merge';
12
13
  import { createVersionedMap, findInMap } from '@utils/collections/util';
13
14
  import type { Node } from '@xyflow/react';
@@ -199,7 +200,11 @@ export const getNodesAndEdges = async ({
199
200
  edges = new Map();
200
201
 
201
202
  // 1. Parallel Fetching
202
- const [domains, services] = await Promise.all([getCollection('domains'), getCollection('services')]);
203
+ const [domains, services, dataProducts] = await Promise.all([
204
+ getCollection('domains'),
205
+ getCollection('services'),
206
+ getCollection('data-products'),
207
+ ]);
203
208
 
204
209
  const domain = domains.find((service) => service.data.id === id && service.data.version === version);
205
210
 
@@ -214,9 +219,11 @@ export const getNodesAndEdges = async ({
214
219
  // 2. Build optimized maps
215
220
  const serviceMap = createVersionedMap(services);
216
221
  const domainMap = createVersionedMap(domains);
222
+ const dataProductMap = createVersionedMap(dataProducts);
217
223
 
218
224
  const rawServices = domain?.data.services || [];
219
225
  const rawSubDomains = domain?.data.domains || [];
226
+ const rawDataProducts = (domain?.data as any)['data-products'] || [];
220
227
 
221
228
  // Optimized hydration
222
229
  const domainServicesWithVersion = rawServices
@@ -229,7 +236,12 @@ export const getNodesAndEdges = async ({
229
236
  .filter((d): d is any => !!d)
230
237
  .map((svc) => ({ id: svc.data.id, version: svc.data.version }));
231
238
 
232
- // Get all the nodes for everyhing
239
+ const domainDataProductsWithVersion = rawDataProducts
240
+ .map((dataProduct: any) => findInMap(dataProductMap, dataProduct.id, dataProduct.version))
241
+ .filter((dp: any): dp is any => !!dp)
242
+ .map((dp: any) => ({ id: dp.data.id, version: dp.data.version }));
243
+
244
+ // Get all the nodes for everything
233
245
 
234
246
  for (const service of domainServicesWithVersion) {
235
247
  const { nodes: serviceNodes, edges: serviceEdges } = await getServicesNodeAndEdges({
@@ -255,6 +267,20 @@ export const getNodesAndEdges = async ({
255
267
  serviceEdges.forEach((e) => edges.set(e.id, e));
256
268
  }
257
269
 
270
+ for (const dataProduct of domainDataProductsWithVersion) {
271
+ const { nodes: dataProductNodes, edges: dataProductEdges } = await getDataProductsNodeAndEdges({
272
+ id: dataProduct.id,
273
+ version: dataProduct.version,
274
+ defaultFlow: flow,
275
+ mode,
276
+ });
277
+ dataProductNodes.forEach((n: any) => {
278
+ nodes.set(n.id, nodes.has(n.id) ? merge(nodes.get(n.id), n) : n);
279
+ });
280
+ // @ts-ignore
281
+ dataProductEdges.forEach((e) => edges.set(e.id, e));
282
+ }
283
+
258
284
  for (const subDomain of domainSubDomainsWithVersion) {
259
285
  const { nodes: subDomainNodes, edges: subDomainEdges } = await getNodesAndEdges({
260
286
  id: subDomain.id,