@eventcatalog/core 3.7.1 → 3.8.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.
Files changed (67) 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-44EDP7IH.js → chunk-4EJDLNIX.js} +1 -1
  6. package/dist/{chunk-MIHGWXCX.js → chunk-EG36OTR7.js} +1 -1
  7. package/dist/{chunk-BVJJ3COQ.js → chunk-GITARDPK.js} +1 -1
  8. package/dist/{chunk-PV5ER42D.js → chunk-IEEU454Z.js} +1 -1
  9. package/dist/{chunk-B6NBNZGS.js → chunk-ZIG6J4R2.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 +107 -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 +51 -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 +33 -11
  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/services.ts +2 -26
  61. package/eventcatalog/src/utils/collections/util.ts +75 -2
  62. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +91 -3
  63. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +225 -0
  64. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +28 -2
  65. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +74 -20
  66. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  67. package/package.json +2 -2
@@ -1,7 +1,8 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import path from 'path';
4
- import { createVersionedMap, findInMap, satisfies } from './util';
4
+ import { createVersionedMap } from './util';
5
+ import { hydrateProducersAndConsumers } from './messages';
5
6
  import utils from '@eventcatalog/sdk';
6
7
 
7
8
  const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
@@ -35,10 +36,11 @@ export const getEvents = async ({ getAllVersions = true, hydrateServices = true
35
36
  }
36
37
 
37
38
  // 1. Fetch collections in parallel
38
- const [allEvents, allServices, allChannels] = await Promise.all([
39
+ const [allEvents, allServices, allChannels, allDataProducts] = await Promise.all([
39
40
  getCollection('events'),
40
41
  getCollection('services'),
41
42
  getCollection('channels'),
43
+ getCollection('data-products'),
42
44
  ]);
43
45
 
44
46
  // 2. Build optimized maps
@@ -64,33 +66,13 @@ export const getEvents = async ({ getAllVersions = true, hydrateServices = true
64
66
  const latestVersion = eventVersions[0]?.data.version || event.data.version;
65
67
  const versions = eventVersions.map((e) => e.data.version);
66
68
 
67
- // Find Producers (Services that send this event)
68
- const producers = allServices
69
- .filter((service) =>
70
- service.data.sends?.some((item) => {
71
- if (item.id !== event.data.id) return false;
72
- if (item.version === 'latest' || item.version === undefined) return event.data.version === latestVersion;
73
- return satisfies(event.data.version, item.version);
74
- })
75
- )
76
- .map((service) => {
77
- if (!hydrateServices) return { id: service.data.id, version: service.data.version };
78
- return service;
79
- });
80
-
81
- // Find Consumers (Services that receive this event)
82
- const consumers = allServices
83
- .filter((service) =>
84
- service.data.receives?.some((item) => {
85
- if (item.id !== event.data.id) return false;
86
- if (item.version === 'latest' || item.version === undefined) return event.data.version === latestVersion;
87
- return satisfies(event.data.version, item.version);
88
- })
89
- )
90
- .map((service) => {
91
- if (!hydrateServices) return { id: service.data.id, version: service.data.version };
92
- return service;
93
- });
69
+ // Find producers and consumers (services + data products)
70
+ const { producers, consumers } = hydrateProducersAndConsumers({
71
+ message: { data: { ...event.data, latestVersion } },
72
+ services: allServices,
73
+ dataProducts: allDataProducts,
74
+ hydrate: hydrateServices,
75
+ });
94
76
 
95
77
  // Find Channels
96
78
  const messageChannels = event.data.channels || [];
@@ -10,6 +10,7 @@ import {
10
10
  ArrowsRightLeftIcon,
11
11
  VariableIcon,
12
12
  MapIcon,
13
+ CubeIcon,
13
14
  } from '@heroicons/react/24/outline';
14
15
  import { BookText, Box, DatabaseIcon } from 'lucide-react';
15
16
 
@@ -43,6 +44,8 @@ export const getIconForCollection = (collection: string) => {
43
44
  return Box;
44
45
  case 'containers':
45
46
  return DatabaseIcon;
47
+ case 'data-products':
48
+ return CubeIcon;
46
49
  default:
47
50
  return ServerIcon;
48
51
  }
@@ -74,6 +77,8 @@ export const getColorAndIconForCollection = (collection: string) => {
74
77
  return { color: 'yellow', Icon: icon };
75
78
  case 'services':
76
79
  return { color: 'pink', Icon: icon };
80
+ case 'data-products':
81
+ return { color: 'cyan', Icon: icon };
77
82
  default:
78
83
  return { color: 'gray', Icon: icon };
79
84
  }
@@ -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 || [];
@@ -6,7 +6,7 @@ import type { CollectionMessageTypes, CollectionTypes } from '@types';
6
6
  const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
7
7
  import utils, { type Domain } from '@eventcatalog/sdk';
8
8
  import { getDomains, getDomainsForService } from './domains';
9
- import { createVersionedMap, findInMap } from '@utils/collections/util';
9
+ import { createVersionedMap, findInMap, processSpecifications } from '@utils/collections/util';
10
10
 
11
11
  export type Service = CollectionEntry<'services'>;
12
12
 
@@ -174,31 +174,7 @@ export const getConsumersOfMessage = (services: Service[], message: CollectionEn
174
174
  };
175
175
 
176
176
  export const getSpecificationsForService = (service: CollectionEntry<CollectionTypes>) => {
177
- const specifications = Array.isArray(service.data.specifications) ? service.data.specifications : [];
178
-
179
- if (service.data.specifications && !Array.isArray(service.data.specifications)) {
180
- if (service.data.specifications.asyncapiPath) {
181
- specifications.push({
182
- type: 'asyncapi',
183
- path: service.data.specifications.asyncapiPath,
184
- name: 'AsyncAPI',
185
- });
186
- }
187
- if (service.data.specifications.openapiPath) {
188
- specifications.push({
189
- type: 'openapi',
190
- path: service.data.specifications.openapiPath,
191
- name: 'OpenAPI',
192
- });
193
- }
194
- }
195
-
196
- return specifications.map((spec) => ({
197
- ...spec,
198
- name: spec.name || (spec.type === 'asyncapi' ? 'AsyncAPI' : 'OpenAPI'),
199
- filename: path.basename(spec.path),
200
- filenameWithoutExtension: path.basename(spec.path, path.extname(spec.path)),
201
- }));
177
+ return processSpecifications(service.data.specifications as any);
202
178
  };
203
179
  // Get services for channel
204
180
  export const getProducersAndConsumersForChannel = async (channel: CollectionEntry<'channels'>) => {
@@ -1,6 +1,70 @@
1
1
  import type { CollectionTypes } from '@types';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import semver, { coerce, compare, eq, satisfies as satisfiesRange } from 'semver';
4
+ import path from 'node:path';
5
+
6
+ // --- SPECIFICATION HELPERS ---
7
+
8
+ export type SpecificationType = 'asyncapi' | 'openapi' | 'graphql';
9
+
10
+ export interface SpecificationInput {
11
+ type: SpecificationType;
12
+ path: string;
13
+ name?: string;
14
+ }
15
+
16
+ export interface ProcessedSpecification {
17
+ type: SpecificationType;
18
+ path: string;
19
+ name: string;
20
+ filename: string;
21
+ filenameWithoutExtension: string;
22
+ }
23
+
24
+ export const getDefaultSpecificationName = (type: string): string => {
25
+ switch (type) {
26
+ case 'asyncapi':
27
+ return 'AsyncAPI';
28
+ case 'openapi':
29
+ return 'OpenAPI';
30
+ case 'graphql':
31
+ return 'GraphQL';
32
+ default:
33
+ return 'Specification';
34
+ }
35
+ };
36
+
37
+ interface LegacySpecificationFormat {
38
+ asyncapiPath?: string;
39
+ openapiPath?: string;
40
+ graphqlPath?: string;
41
+ }
42
+
43
+ export const processSpecifications = (
44
+ specifications: SpecificationInput[] | LegacySpecificationFormat | undefined
45
+ ): ProcessedSpecification[] => {
46
+ const specs: SpecificationInput[] = Array.isArray(specifications) ? [...specifications] : [];
47
+
48
+ // Handle legacy object format
49
+ if (specifications && !Array.isArray(specifications)) {
50
+ if (specifications.asyncapiPath) {
51
+ specs.push({ type: 'asyncapi', path: specifications.asyncapiPath, name: 'AsyncAPI' });
52
+ }
53
+ if (specifications.openapiPath) {
54
+ specs.push({ type: 'openapi', path: specifications.openapiPath, name: 'OpenAPI' });
55
+ }
56
+ if (specifications.graphqlPath) {
57
+ specs.push({ type: 'graphql', path: specifications.graphqlPath, name: 'GraphQL' });
58
+ }
59
+ }
60
+
61
+ return specs.map((spec) => ({
62
+ ...spec,
63
+ name: spec.name || getDefaultSpecificationName(spec.type),
64
+ filename: path.basename(spec.path),
65
+ filenameWithoutExtension: path.basename(spec.path, path.extname(spec.path)),
66
+ }));
67
+ };
4
68
 
5
69
  export const getPreviousVersion = (version: string, versions: string[]) => {
6
70
  const index = versions.indexOf(version);
@@ -94,8 +158,8 @@ export const getItemsFromCollectionByIdAndSemverOrLatest = <T extends { data: {
94
158
  };
95
159
 
96
160
  export const findMatchingNodes = (
97
- nodesA: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers'>[],
98
- 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'>[]
99
163
  ) => {
100
164
  // Track messages that are both sent and received
101
165
  return nodesA.filter((nodeA) => {
@@ -118,6 +182,7 @@ export const resourceToCollectionMap = {
118
182
  container: 'containers',
119
183
  entity: 'entities',
120
184
  diagram: 'diagrams',
185
+ 'data-product': 'data-products',
121
186
  } as const;
122
187
 
123
188
  export const collectionToResourceMap = {
@@ -133,6 +198,7 @@ export const collectionToResourceMap = {
133
198
  containers: 'container',
134
199
  entities: 'entity',
135
200
  diagrams: 'diagram',
201
+ 'data-products': 'data-product',
136
202
  } as const;
137
203
 
138
204
  export const getDeprecatedDetails = (item: CollectionEntry<CollectionTypes>) => {
@@ -197,6 +263,13 @@ export const createVersionedMap = <T extends { data: { id: string; version?: str
197
263
  return map;
198
264
  };
199
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
+
200
273
  /**
201
274
  * Fast lookup helper.
202
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
  });