@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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-M7EPRGHR.js → chunk-4BEERWPE.js} +1 -1
- package/dist/{chunk-GQZVIS3Z.js → chunk-NV7H3356.js} +1 -1
- package/dist/{chunk-7CTNGTBB.js → chunk-POQENB7N.js} +1 -1
- package/dist/{chunk-WAX3S32H.js → chunk-SL2YYN6D.js} +1 -1
- package/dist/{chunk-O6SRHGZ7.js → chunk-ZBC6HM3V.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +13 -1
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +109 -6
- package/eventcatalog/src/components/Grids/utils.tsx +10 -1
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +4 -0
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +132 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +29 -2
- package/eventcatalog/src/components/SchemaExplorer/types.ts +5 -1
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +3 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +1 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +23 -1
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +62 -0
- package/eventcatalog/src/content.config.ts +34 -0
- package/eventcatalog/src/enterprise/ai/chat-api.ts +26 -0
- package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +1 -1
- package/eventcatalog/src/enterprise/tools/catalog-tools.ts +169 -2
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -1
- package/eventcatalog/src/pages/discover/[type]/index.astro +57 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -1
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +27 -3
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +74 -25
- package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +55 -1
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +10 -1
- package/eventcatalog/src/stores/sidebar-store/builders/container.ts +23 -16
- package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +130 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +11 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +68 -13
- package/eventcatalog/src/styles/theme.css +4 -0
- package/eventcatalog/src/styles/themes/forest.css +4 -0
- package/eventcatalog/src/styles/themes/ocean.css +4 -0
- package/eventcatalog/src/styles/themes/sapphire.css +4 -0
- package/eventcatalog/src/styles/themes/sunset.css +4 -0
- package/eventcatalog/src/types/index.ts +4 -2
- package/eventcatalog/src/utils/collections/commands.ts +11 -29
- package/eventcatalog/src/utils/collections/containers.ts +25 -1
- package/eventcatalog/src/utils/collections/data-products.ts +85 -0
- package/eventcatalog/src/utils/collections/domains.ts +28 -10
- package/eventcatalog/src/utils/collections/events.ts +11 -29
- package/eventcatalog/src/utils/collections/icons.ts +5 -0
- package/eventcatalog/src/utils/collections/messages.ts +68 -0
- package/eventcatalog/src/utils/collections/queries.ts +11 -29
- package/eventcatalog/src/utils/collections/util.ts +11 -2
- package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +91 -3
- package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +225 -0
- package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +28 -2
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +74 -20
- package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
- 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
|
|
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
|
|
64
|
-
const producers =
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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([
|
|
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
|
-
|
|
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,
|