@eventcatalog/core 3.7.2 → 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.
- 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-O6SRHGZ7.js → chunk-4EJDLNIX.js} +1 -1
- package/dist/{chunk-WAX3S32H.js → chunk-EG36OTR7.js} +1 -1
- package/dist/{chunk-GQZVIS3Z.js → chunk-GITARDPK.js} +1 -1
- package/dist/{chunk-7CTNGTBB.js → chunk-IEEU454Z.js} +1 -1
- package/dist/{chunk-M7EPRGHR.js → chunk-ZIG6J4R2.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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { QueueListIcon, RectangleGroupIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { QueueListIcon, RectangleGroupIcon, BoltIcon, ChatBubbleLeftIcon, CubeIcon } from '@heroicons/react/24/outline';
|
|
3
3
|
import ServerIcon from '@heroicons/react/24/outline/ServerIcon';
|
|
4
4
|
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid';
|
|
5
5
|
import { DatabaseIcon } from 'lucide-react';
|
|
@@ -10,8 +10,10 @@ import { getEvents } from '@utils/collections/events';
|
|
|
10
10
|
import { getServices } from '@utils/collections/services';
|
|
11
11
|
import { getQueries } from '@utils/collections/queries';
|
|
12
12
|
import { getContainers } from '@utils/collections/containers';
|
|
13
|
+
import { getDataProducts } from '@utils/collections/data-products';
|
|
13
14
|
import { getUsers } from '@utils/collections/users';
|
|
14
15
|
import { getTeams } from '@utils/collections/teams';
|
|
16
|
+
import { getChannels } from '@utils/collections/channels';
|
|
15
17
|
import { buildUrl } from '@utils/url-builder';
|
|
16
18
|
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
17
19
|
import { DiscoverTable, type DiscoverTableData, type CollectionType } from '@components/Tables/Discover';
|
|
@@ -31,6 +33,8 @@ const services = await getServices();
|
|
|
31
33
|
const domains = await getDomains({ getAllVersions: false });
|
|
32
34
|
const flows = await getFlows();
|
|
33
35
|
const containers = await getContainers();
|
|
36
|
+
const dataProducts = await getDataProducts();
|
|
37
|
+
const channels = await getChannels();
|
|
34
38
|
const users = await getUsers();
|
|
35
39
|
const teams = await getTeams();
|
|
36
40
|
|
|
@@ -108,6 +112,15 @@ const typeConfig: Record<
|
|
|
108
112
|
{ id: 'isDeprecated', label: 'Is Deprecated' },
|
|
109
113
|
],
|
|
110
114
|
},
|
|
115
|
+
'data-products': {
|
|
116
|
+
label: 'Data Products',
|
|
117
|
+
propertyOptions: [
|
|
118
|
+
{ id: 'hasOwners', label: 'Has Owners' },
|
|
119
|
+
{ id: 'hasInputs', label: 'Has Inputs' },
|
|
120
|
+
{ id: 'hasOutputs', label: 'Has Outputs' },
|
|
121
|
+
{ id: 'isDeprecated', label: 'Is Deprecated' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
111
124
|
};
|
|
112
125
|
|
|
113
126
|
const currentTypeConfig = typeConfig[type] || typeConfig.events;
|
|
@@ -143,6 +156,15 @@ const tabs = [
|
|
|
143
156
|
enabled: containers.length > 0,
|
|
144
157
|
visible: containers.length > 0,
|
|
145
158
|
},
|
|
159
|
+
{
|
|
160
|
+
label: `Data Products (${dataProducts.length})`,
|
|
161
|
+
href: buildUrl('/discover/data-products'),
|
|
162
|
+
isActive: type === 'data-products',
|
|
163
|
+
icon: CubeIcon,
|
|
164
|
+
activeColor: 'cyan',
|
|
165
|
+
enabled: dataProducts.length > 0,
|
|
166
|
+
visible: dataProducts.length > 0,
|
|
167
|
+
},
|
|
146
168
|
{
|
|
147
169
|
label: `Events (${events.length})`,
|
|
148
170
|
href: buildUrl('/discover/events'),
|
|
@@ -222,6 +244,35 @@ function hasSpecifications(service: any): boolean {
|
|
|
222
244
|
return !!(specs.openapiPath || specs.asyncapiPath || specs.graphqlPath);
|
|
223
245
|
}
|
|
224
246
|
|
|
247
|
+
// Build lookup maps for all collections (for resolving data product inputs/outputs)
|
|
248
|
+
const allCollections = [
|
|
249
|
+
...services.map((s) => ({ ...s, collection: 'services' })),
|
|
250
|
+
...containers.map((c) => ({ ...c, collection: 'containers' })),
|
|
251
|
+
...channels.map((c) => ({ ...c, collection: 'channels' })),
|
|
252
|
+
...events.map((e) => ({ ...e, collection: 'events' })),
|
|
253
|
+
...commands.map((c) => ({ ...c, collection: 'commands' })),
|
|
254
|
+
...queries.map((q) => ({ ...q, collection: 'queries' })),
|
|
255
|
+
...dataProducts.map((dp) => ({ ...dp, collection: 'data-products' })),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// Create a map for quick lookup by id
|
|
259
|
+
const collectionItemMap = new Map(allCollections.map((item) => [item.data.id, item]));
|
|
260
|
+
|
|
261
|
+
// Helper to resolve a pointer to a collection item
|
|
262
|
+
function resolvePointer(pointer: { id: string; version?: string }) {
|
|
263
|
+
if (!pointer?.id) return null;
|
|
264
|
+
const item = collectionItemMap.get(pointer.id);
|
|
265
|
+
if (!item) return null;
|
|
266
|
+
return {
|
|
267
|
+
collection: item.collection,
|
|
268
|
+
data: {
|
|
269
|
+
id: item.data.id,
|
|
270
|
+
name: item.data.name,
|
|
271
|
+
version: item.data.version,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
225
276
|
// Build a Set of subdomain IDs (domains that are nested within other domains)
|
|
226
277
|
const allSubdomainIds = new Set(
|
|
227
278
|
domains.flatMap((d: any) => (d.data?.domains || []).map((sd: any) => sd.data?.id || sd.id)).filter(Boolean)
|
|
@@ -256,6 +307,9 @@ const tableData = enrichedData.map((d: any) => ({
|
|
|
256
307
|
hasSpecifications: type === 'services' ? hasSpecifications(d) : false,
|
|
257
308
|
hasRepository: type === 'services' ? !!d.data?.repository?.url : false,
|
|
258
309
|
hasDataDependencies: type === 'services' ? (d.data?.writesTo || []).length > 0 || (d.data?.readsFrom || []).length > 0 : false,
|
|
310
|
+
// Data-product-specific properties
|
|
311
|
+
hasInputs: type === 'data-products' ? (d.data?.inputs || []).length > 0 : false,
|
|
312
|
+
hasOutputs: type === 'data-products' ? (d.data?.outputs || []).length > 0 : false,
|
|
259
313
|
isDeprecated: d.data?.deprecated === true || (typeof d.data?.deprecated === 'object' && d.data?.deprecated !== null),
|
|
260
314
|
data: {
|
|
261
315
|
id: d.data.id,
|
|
@@ -272,6 +326,8 @@ const tableData = enrichedData.map((d: any) => ({
|
|
|
272
326
|
services: d.data?.services?.map(mapToItem) ?? [],
|
|
273
327
|
servicesThatWriteToContainer: d.data?.servicesThatWriteToContainer?.map(mapToItem) ?? [],
|
|
274
328
|
servicesThatReadFromContainer: d.data?.servicesThatReadFromContainer?.map(mapToItem) ?? [],
|
|
329
|
+
inputs: d.data?.inputs?.map(resolvePointer).filter(Boolean) ?? [],
|
|
330
|
+
outputs: d.data?.outputs?.map(resolvePointer).filter(Boolean) ?? [],
|
|
275
331
|
},
|
|
276
332
|
}));
|
|
277
333
|
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
ClockIcon,
|
|
29
29
|
} from '@heroicons/react/24/outline';
|
|
30
30
|
import { ArrowsRightLeftIcon } from '@heroicons/react/20/solid';
|
|
31
|
-
import { Box, Boxes, SquarePenIcon, DatabaseIcon, DatabaseZapIcon, ShieldCheckIcon, AlignLeft } from 'lucide-react';
|
|
31
|
+
import { Box, Boxes, SquarePenIcon, DatabaseIcon, DatabaseZapIcon, ShieldCheckIcon, AlignLeft, Package } from 'lucide-react';
|
|
32
32
|
|
|
33
33
|
import { getSpecificationsForService } from '@utils/collections/services';
|
|
34
34
|
import { resourceToCollectionMap, collectionToResourceMap, getDeprecatedDetails } from '@utils/collections/util';
|
|
@@ -160,6 +160,10 @@ const getBadge = () => {
|
|
|
160
160
|
return entityBadges;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
if (props.collection === 'data-products') {
|
|
164
|
+
return [{ backgroundColor: 'indigo', textColor: 'indigo', content: 'Data Product', icon: Package }];
|
|
165
|
+
}
|
|
166
|
+
|
|
163
167
|
return [{ backgroundColor: 'teal', textColor: 'teal', content: '', icon: QueueListIcon, class: 'text-gray' }];
|
|
164
168
|
};
|
|
165
169
|
|
|
@@ -28,8 +28,8 @@ export class Page extends HybridPage {
|
|
|
28
28
|
// We only care about any item that has data.schemaPath
|
|
29
29
|
const itemsWithSchema = allItems.flatMap((items) => items.filter((item) => item.data.schemaPath));
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Generate paths for messages with schemas
|
|
32
|
+
const messagePaths = itemsWithSchema.map((item) => ({
|
|
33
33
|
params: {
|
|
34
34
|
type: item.collection,
|
|
35
35
|
id: item.data.id,
|
|
@@ -42,7 +42,31 @@ export class Page extends HybridPage {
|
|
|
42
42
|
body: undefined,
|
|
43
43
|
},
|
|
44
44
|
}));
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
// Generate paths for data products with contracts
|
|
47
|
+
const dataProducts = await pageDataLoader['data-products']();
|
|
48
|
+
const dataProductPaths = dataProducts.flatMap((dataProduct) => {
|
|
49
|
+
const outputs = (dataProduct.data as any).outputs || [];
|
|
50
|
+
const outputsWithContracts = outputs.filter((output: any) => output.contract);
|
|
51
|
+
|
|
52
|
+
return outputsWithContracts.map((output: any) => ({
|
|
53
|
+
params: {
|
|
54
|
+
type: 'data-products',
|
|
55
|
+
id: dataProduct.data.id,
|
|
56
|
+
version: dataProduct.data.version,
|
|
57
|
+
},
|
|
58
|
+
props: {
|
|
59
|
+
type: 'data-products',
|
|
60
|
+
...dataProduct,
|
|
61
|
+
contractPath: output.contract.path,
|
|
62
|
+
contractName: output.contract.name,
|
|
63
|
+
contractType: output.contract.type,
|
|
64
|
+
body: undefined,
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return [...messagePaths, ...dataProductPaths];
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
protected static async fetchData(params: any) {
|
|
@@ -6,8 +6,8 @@ import SchemaPageViewer from '@components/SchemaExplorer/SchemaPageViewer';
|
|
|
6
6
|
import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
|
|
7
7
|
import { sortVersioned } from '@utils/collections/util';
|
|
8
8
|
import { isEventCatalogScaleEnabled } from '@utils/feature';
|
|
9
|
-
import
|
|
10
|
-
import path from 'path';
|
|
9
|
+
import { resourceFileExists, readResourceFile } from '@utils/resource-files';
|
|
10
|
+
import path from 'node:path';
|
|
11
11
|
import type { SchemaItem } from '@components/SchemaExplorer/types';
|
|
12
12
|
|
|
13
13
|
export const prerender = Page.prerender;
|
|
@@ -16,27 +16,76 @@ export const getStaticPaths = Page.getStaticPaths;
|
|
|
16
16
|
// Get data
|
|
17
17
|
const props = await Page.getData(Astro);
|
|
18
18
|
const { type, data } = props as { type: PageTypes; data: any };
|
|
19
|
-
const pageTitle = `${type} | ${data.name}`.replace(/^\w/, (c) => c.toUpperCase());
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
const
|
|
20
|
+
// Check if this is a data product with a contract
|
|
21
|
+
const isDataProduct = type === 'data-products';
|
|
22
|
+
// Try query param first (SSR mode), then fall back to props (static mode)
|
|
23
|
+
const contractPath = Astro.url.searchParams.get('contract') || (props as any).contractPath;
|
|
24
|
+
|
|
25
|
+
let currentMessage: SchemaItem | null = null;
|
|
26
|
+
let sortedVersions: SchemaItem[] = [];
|
|
27
|
+
let pageTitle = `${type} | ${data.name}`.replace(/^\w/, (c) => c.toUpperCase());
|
|
28
|
+
|
|
29
|
+
if (isDataProduct && contractPath) {
|
|
30
|
+
// Handle data product contracts
|
|
31
|
+
const allDataProducts = await pageDataLoader['data-products']();
|
|
32
|
+
const dataProductVersions = allDataProducts.filter((item) => item.data.id === data.id);
|
|
33
|
+
|
|
34
|
+
// Find contracts across all versions
|
|
35
|
+
const contractVersions = dataProductVersions
|
|
36
|
+
.map((dataProduct) => {
|
|
37
|
+
const outputs = (dataProduct.data as any).outputs || [];
|
|
38
|
+
const matchingOutput = outputs.find((output: any) => output.contract?.path === contractPath);
|
|
39
|
+
|
|
40
|
+
if (!matchingOutput) return null;
|
|
41
|
+
|
|
42
|
+
if (!resourceFileExists(dataProduct, contractPath)) return null;
|
|
23
43
|
|
|
24
|
-
// Transform to SchemaItems
|
|
25
|
-
const availableVersions = await Promise.all(
|
|
26
|
-
versions
|
|
27
|
-
.filter((message) => message.data.schemaPath)
|
|
28
|
-
.filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? '')))
|
|
29
|
-
.map(async (message) => {
|
|
30
44
|
try {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
45
|
+
const schemaContent = readResourceFile(dataProduct, contractPath) ?? '';
|
|
46
|
+
const schemaExtension = path.extname(contractPath).slice(1);
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
return {
|
|
49
|
+
collection: 'data-products',
|
|
50
|
+
data: {
|
|
51
|
+
id: dataProduct.data.id,
|
|
52
|
+
name: matchingOutput.contract.name,
|
|
53
|
+
version: dataProduct.data.version,
|
|
54
|
+
summary: `Data contract for ${dataProduct.data.name}`,
|
|
55
|
+
schemaPath: contractPath,
|
|
56
|
+
},
|
|
57
|
+
schemaContent,
|
|
58
|
+
schemaExtension,
|
|
59
|
+
contractType: matchingOutput.contract.type,
|
|
60
|
+
dataProductId: dataProduct.data.id,
|
|
61
|
+
dataProductVersion: dataProduct.data.version,
|
|
62
|
+
} as SchemaItem;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`Error reading contract for ${dataProduct.data.id}:`, error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.filter((v): v is SchemaItem => v !== null);
|
|
38
69
|
|
|
39
|
-
|
|
70
|
+
sortedVersions = sortVersioned(contractVersions, (item) => item.data.version);
|
|
71
|
+
currentMessage = sortedVersions.find((v) => v.data.version === data.version) ?? null;
|
|
72
|
+
|
|
73
|
+
if (currentMessage) {
|
|
74
|
+
pageTitle = `Data Contract | ${currentMessage.data.name}`;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Handle regular messages (events, commands, queries)
|
|
78
|
+
const allItems = await pageDataLoader[type]();
|
|
79
|
+
const versions = allItems.filter((item) => item.data.id === data.id);
|
|
80
|
+
|
|
81
|
+
// Transform to SchemaItems
|
|
82
|
+
const availableVersions = versions
|
|
83
|
+
.filter((message) => message.data.schemaPath && resourceFileExists(message, message.data.schemaPath))
|
|
84
|
+
.map((message) => {
|
|
85
|
+
try {
|
|
86
|
+
const schemaPath = message.data.schemaPath ?? '';
|
|
87
|
+
const schemaContent = readResourceFile(message, schemaPath) ?? '';
|
|
88
|
+
const schemaExtension = path.extname(schemaPath).slice(1);
|
|
40
89
|
|
|
41
90
|
return {
|
|
42
91
|
collection: message.collection,
|
|
@@ -59,14 +108,12 @@ const availableVersions = await Promise.all(
|
|
|
59
108
|
return null;
|
|
60
109
|
}
|
|
61
110
|
})
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const validVersions = availableVersions.filter((v): v is SchemaItem => v !== null);
|
|
111
|
+
.filter((v): v is SchemaItem => v !== null);
|
|
65
112
|
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
sortedVersions = sortVersioned(availableVersions, (item) => item.data.version);
|
|
114
|
+
currentMessage = sortedVersions.find((v) => v.data.version === data.version) ?? null;
|
|
115
|
+
}
|
|
68
116
|
|
|
69
|
-
const currentMessage = sortedVersions.find((v) => v.data.version === data.version);
|
|
70
117
|
const apiAccessEnabled = isEventCatalogScaleEnabled();
|
|
71
118
|
---
|
|
72
119
|
|
|
@@ -83,7 +130,9 @@ const apiAccessEnabled = isEventCatalogScaleEnabled();
|
|
|
83
130
|
showProducersConsumers={false}
|
|
84
131
|
/>
|
|
85
132
|
) : (
|
|
86
|
-
<div class="p-8 flex items-center justify-center h-full text-
|
|
133
|
+
<div class="p-8 flex items-center justify-center h-full text-[rgb(var(--ec-page-text-muted))]">
|
|
134
|
+
Schema not found or could not be loaded.
|
|
135
|
+
</div>
|
|
87
136
|
)
|
|
88
137
|
}
|
|
89
138
|
</div>
|
|
@@ -5,6 +5,7 @@ import { getCommands } from '@utils/collections/commands';
|
|
|
5
5
|
import { getQueries } from '@utils/collections/queries';
|
|
6
6
|
import { getServices, getSpecificationsForService } from '@utils/collections/services';
|
|
7
7
|
import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
|
|
8
|
+
import { getDataProducts } from '@utils/collections/data-products';
|
|
8
9
|
import { getOwner } from '@utils/collections/owners';
|
|
9
10
|
import { buildUrl } from '@utils/url-builder';
|
|
10
11
|
import { resourceFileExists, readResourceFile } from '@utils/resource-files';
|
|
@@ -185,7 +186,60 @@ async function fetchAllSchemas() {
|
|
|
185
186
|
// Flatten and filter out null values for domains
|
|
186
187
|
const flatDomainsWithSpecs = domainsWithSpecs.flat().filter((domain) => domain !== null);
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
// Fetch all data products and extract contracts from outputs
|
|
190
|
+
const dataProducts = await getDataProducts({ getAllVersions: true });
|
|
191
|
+
|
|
192
|
+
// Filter data products with contracts in outputs and read contract content
|
|
193
|
+
const dataProductsWithContracts = await Promise.all(
|
|
194
|
+
dataProducts.map(async (dataProduct) => {
|
|
195
|
+
try {
|
|
196
|
+
const outputs = dataProduct.data.outputs || [];
|
|
197
|
+
const outputsWithContracts = outputs.filter((output) => output.contract);
|
|
198
|
+
|
|
199
|
+
if (outputsWithContracts.length === 0) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return await Promise.all(
|
|
204
|
+
outputsWithContracts.map(async (output) => {
|
|
205
|
+
const contract = output.contract!;
|
|
206
|
+
if (!resourceFileExists(dataProduct, contract.path)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const schemaContent = readResourceFile(dataProduct, contract.path) ?? '';
|
|
211
|
+
const schemaExtension = path.extname(contract.path).slice(1) || 'json';
|
|
212
|
+
const enrichedOwners = await enrichOwners(dataProduct.data.owners || []);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
collection: 'data-products',
|
|
216
|
+
data: {
|
|
217
|
+
id: `${dataProduct.data.id}__${contract.path}`,
|
|
218
|
+
name: contract.name,
|
|
219
|
+
version: dataProduct.data.version,
|
|
220
|
+
summary: `Data contract for ${dataProduct.data.name}`,
|
|
221
|
+
schemaPath: contract.path,
|
|
222
|
+
owners: enrichedOwners,
|
|
223
|
+
},
|
|
224
|
+
schemaContent,
|
|
225
|
+
schemaExtension,
|
|
226
|
+
contractType: contract.type,
|
|
227
|
+
dataProductId: dataProduct.data.id,
|
|
228
|
+
dataProductVersion: dataProduct.data.version,
|
|
229
|
+
};
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`Error reading contracts for data product ${dataProduct.data.id}:`, error);
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Flatten and filter out null values for data product contracts
|
|
240
|
+
const flatDataProductContracts = dataProductsWithContracts.flat().filter((contract) => contract !== null);
|
|
241
|
+
|
|
242
|
+
return [...messagesWithSchemas, ...flatServicesWithSpecs, ...flatDomainsWithSpecs, ...flatDataProductContracts];
|
|
189
243
|
}
|
|
190
244
|
|
|
191
245
|
export class Page extends HybridPage {
|
|
@@ -20,7 +20,16 @@ export class Page extends HybridPage {
|
|
|
20
20
|
flows: getFlows,
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const itemTypes: PageTypesWithFlows[] = [
|
|
23
|
+
const itemTypes: PageTypesWithFlows[] = [
|
|
24
|
+
'events',
|
|
25
|
+
'commands',
|
|
26
|
+
'queries',
|
|
27
|
+
'services',
|
|
28
|
+
'domains',
|
|
29
|
+
'flows',
|
|
30
|
+
'containers',
|
|
31
|
+
'data-products',
|
|
32
|
+
];
|
|
24
33
|
const allItems = await Promise.all(itemTypes.map((type) => loaders[type]()));
|
|
25
34
|
|
|
26
35
|
return allItems.flatMap((items, index) =>
|
|
@@ -18,11 +18,22 @@ export const buildContainerNode = (
|
|
|
18
18
|
): NavNode => {
|
|
19
19
|
const servicesWritingToContainer = container.data.servicesThatWriteToContainer || [];
|
|
20
20
|
const servicesReadingFromContainer = container.data.servicesThatReadFromContainer || [];
|
|
21
|
+
const dataProductsWritingToContainer = (container.data as any).dataProductsThatWriteToContainer || [];
|
|
22
|
+
const dataProductsReadingFromContainer = (container.data as any).dataProductsThatReadFromContainer || [];
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// Combine writes: services + data products
|
|
25
|
+
const allWrites = [
|
|
26
|
+
...servicesWritingToContainer.map((s: any) => `service:${s.data.id}:${s.data.version}`),
|
|
27
|
+
...dataProductsWritingToContainer.map((dp: any) => `data-product:${dp.data.id}:${dp.data.version}`),
|
|
28
|
+
];
|
|
29
|
+
const renderWrites = allWrites.length > 0 && shouldRenderSideBarSection(container, 'services');
|
|
30
|
+
|
|
31
|
+
// Combine reads: services + data products
|
|
32
|
+
const allReads = [
|
|
33
|
+
...servicesReadingFromContainer.map((s: any) => `service:${s.data.id}:${s.data.version}`),
|
|
34
|
+
...dataProductsReadingFromContainer.map((dp: any) => `data-product:${dp.data.id}:${dp.data.version}`),
|
|
35
|
+
];
|
|
36
|
+
const renderReads = allReads.length > 0 && shouldRenderSideBarSection(container, 'services');
|
|
26
37
|
|
|
27
38
|
const renderVisualiser = isVisualiserEnabled();
|
|
28
39
|
|
|
@@ -67,21 +78,17 @@ export const buildContainerNode = (
|
|
|
67
78
|
icon: 'FileImage',
|
|
68
79
|
pages: diagramNavItems,
|
|
69
80
|
},
|
|
70
|
-
|
|
81
|
+
renderWrites && {
|
|
71
82
|
type: 'group',
|
|
72
|
-
title: '
|
|
73
|
-
icon: '
|
|
74
|
-
pages:
|
|
75
|
-
(service) => `service:${(service as any).data.id}:${(service as any).data.version}`
|
|
76
|
-
),
|
|
83
|
+
title: 'Writes',
|
|
84
|
+
icon: 'ArrowUpFromLine',
|
|
85
|
+
pages: allWrites,
|
|
77
86
|
},
|
|
78
|
-
|
|
87
|
+
renderReads && {
|
|
79
88
|
type: 'group',
|
|
80
|
-
title: '
|
|
81
|
-
icon: '
|
|
82
|
-
pages:
|
|
83
|
-
(service) => `service:${(service as any).data.id}:${(service as any).data.version}`
|
|
84
|
-
),
|
|
89
|
+
title: 'Reads',
|
|
90
|
+
icon: 'ArrowDownToLine',
|
|
91
|
+
pages: allReads,
|
|
85
92
|
},
|
|
86
93
|
renderOwners && buildOwnersSection(owners),
|
|
87
94
|
renderRepository && buildRepositorySection(container.data.repository as { url: string; language: string }),
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { buildUrl } from '@utils/url-builder';
|
|
3
|
+
import type { NavNode, ChildRef } from './shared';
|
|
4
|
+
import { buildQuickReferenceSection, buildOwnersSection, shouldRenderSideBarSection } from './shared';
|
|
5
|
+
import { isVisualiserEnabled } from '@utils/feature';
|
|
6
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest, sortVersioned } from '@utils/collections/util';
|
|
7
|
+
import { getSchemaFormatFromURL } from '@utils/collections/schemas';
|
|
8
|
+
|
|
9
|
+
interface DataProductContext {
|
|
10
|
+
events: CollectionEntry<'events'>[];
|
|
11
|
+
commands: CollectionEntry<'commands'>[];
|
|
12
|
+
queries: CollectionEntry<'queries'>[];
|
|
13
|
+
services: CollectionEntry<'services'>[];
|
|
14
|
+
containers: CollectionEntry<'containers'>[];
|
|
15
|
+
channels: CollectionEntry<'channels'>[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Get highest version from matched items (semver ranges may return multiple matches)
|
|
19
|
+
const getHighestVersion = <T extends { data: { version: string } }>(items: T[]): T | undefined => {
|
|
20
|
+
if (items.length === 0) return undefined;
|
|
21
|
+
if (items.length === 1) return items[0];
|
|
22
|
+
const sorted = sortVersioned(items, (item) => item.data.version);
|
|
23
|
+
return sorted[0];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Resolve a pointer to its collection type and format as sidebar reference
|
|
27
|
+
// Note: Messages use plural keys (events:, commands:, queries:) while other resources use singular
|
|
28
|
+
const resolvePointerToRef = (pointer: { id: string; version?: string }, context: DataProductContext): string | null => {
|
|
29
|
+
const { id, version } = pointer;
|
|
30
|
+
|
|
31
|
+
// Check each collection type using semver resolution - messages use plural keys in the sidebar
|
|
32
|
+
const events = getItemsFromCollectionByIdAndSemverOrLatest(context.events, id, version);
|
|
33
|
+
const event = getHighestVersion(events);
|
|
34
|
+
if (event) return `events:${id}:${event.data.version}`;
|
|
35
|
+
|
|
36
|
+
const commands = getItemsFromCollectionByIdAndSemverOrLatest(context.commands, id, version);
|
|
37
|
+
const command = getHighestVersion(commands);
|
|
38
|
+
if (command) return `commands:${id}:${command.data.version}`;
|
|
39
|
+
|
|
40
|
+
const queries = getItemsFromCollectionByIdAndSemverOrLatest(context.queries, id, version);
|
|
41
|
+
const query = getHighestVersion(queries);
|
|
42
|
+
if (query) return `queries:${id}:${query.data.version}`;
|
|
43
|
+
|
|
44
|
+
// Non-message resources use singular keys
|
|
45
|
+
const services = getItemsFromCollectionByIdAndSemverOrLatest(context.services, id, version);
|
|
46
|
+
const service = getHighestVersion(services);
|
|
47
|
+
if (service) return `service:${id}:${service.data.version}`;
|
|
48
|
+
|
|
49
|
+
const containers = getItemsFromCollectionByIdAndSemverOrLatest(context.containers, id, version);
|
|
50
|
+
const container = getHighestVersion(containers);
|
|
51
|
+
if (container) return `container:${id}:${container.data.version}`;
|
|
52
|
+
|
|
53
|
+
const channels = getItemsFromCollectionByIdAndSemverOrLatest(context.channels, id, version);
|
|
54
|
+
const channel = getHighestVersion(channels);
|
|
55
|
+
if (channel) return `channel:${id}:${channel.data.version}`;
|
|
56
|
+
|
|
57
|
+
// Unknown type - skip it
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const buildDataProductNode = (
|
|
62
|
+
dataProduct: CollectionEntry<'data-products'>,
|
|
63
|
+
owners: any[],
|
|
64
|
+
context: DataProductContext
|
|
65
|
+
): NavNode => {
|
|
66
|
+
const inputs = dataProduct.data.inputs || [];
|
|
67
|
+
const outputs = dataProduct.data.outputs || [];
|
|
68
|
+
|
|
69
|
+
const renderVisualiser = isVisualiserEnabled();
|
|
70
|
+
const renderOwners = owners.length > 0 && shouldRenderSideBarSection(dataProduct, 'owners');
|
|
71
|
+
|
|
72
|
+
// Resolve inputs and outputs to their proper sidebar references
|
|
73
|
+
const resolvedInputs = inputs.map((input) => resolvePointerToRef(input, context)).filter(Boolean) as string[];
|
|
74
|
+
const resolvedOutputs = outputs.map((output) => resolvePointerToRef(output, context)).filter(Boolean) as string[];
|
|
75
|
+
|
|
76
|
+
// Extract data contracts from outputs that have a contract field
|
|
77
|
+
const dataContracts = outputs
|
|
78
|
+
.filter((output) => output.contract)
|
|
79
|
+
.map((output) => ({
|
|
80
|
+
type: 'item' as const,
|
|
81
|
+
title: `${output.contract!.name} (${getSchemaFormatFromURL(output.contract!.path).toUpperCase()})`,
|
|
82
|
+
summary: output.contract!.type ? `Type: ${output.contract!.type}` : undefined,
|
|
83
|
+
href: buildUrl(
|
|
84
|
+
`/schemas/data-products/${dataProduct.data.id}/${dataProduct.data.version}?contract=${encodeURIComponent(output.contract!.path)}`
|
|
85
|
+
),
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
type: 'item',
|
|
90
|
+
title: dataProduct.data.name,
|
|
91
|
+
badge: 'Data Product',
|
|
92
|
+
summary: dataProduct.data.summary,
|
|
93
|
+
pages: [
|
|
94
|
+
buildQuickReferenceSection([
|
|
95
|
+
{ title: 'Overview', href: buildUrl(`/docs/data-products/${dataProduct.data.id}/${dataProduct.data.version}`) },
|
|
96
|
+
]),
|
|
97
|
+
renderVisualiser && {
|
|
98
|
+
type: 'group',
|
|
99
|
+
title: 'Architecture',
|
|
100
|
+
icon: 'Workflow',
|
|
101
|
+
pages: [
|
|
102
|
+
{
|
|
103
|
+
type: 'item',
|
|
104
|
+
title: 'Map',
|
|
105
|
+
href: buildUrl(`/visualiser/data-products/${dataProduct.data.id}/${dataProduct.data.version}`),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
resolvedInputs.length > 0 && {
|
|
110
|
+
type: 'group',
|
|
111
|
+
title: 'Inputs',
|
|
112
|
+
icon: 'ArrowDownToLine',
|
|
113
|
+
pages: resolvedInputs,
|
|
114
|
+
},
|
|
115
|
+
resolvedOutputs.length > 0 && {
|
|
116
|
+
type: 'group',
|
|
117
|
+
title: 'Outputs',
|
|
118
|
+
icon: 'ArrowUpFromLine',
|
|
119
|
+
pages: resolvedOutputs,
|
|
120
|
+
},
|
|
121
|
+
dataContracts.length > 0 && {
|
|
122
|
+
type: 'group',
|
|
123
|
+
title: 'Data Contracts',
|
|
124
|
+
icon: 'FileCheck',
|
|
125
|
+
pages: dataContracts,
|
|
126
|
+
},
|
|
127
|
+
renderOwners && buildOwnersSection(owners),
|
|
128
|
+
].filter(Boolean) as ChildRef[],
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -18,6 +18,9 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
|
|
|
18
18
|
const servicesInDomain = domain.data.services || [];
|
|
19
19
|
const renderServices = servicesInDomain.length > 0 && shouldRenderSideBarSection(domain, 'services');
|
|
20
20
|
|
|
21
|
+
const dataProductsInDomain = domain.data['data-products'] || [];
|
|
22
|
+
const renderDataProducts = dataProductsInDomain.length > 0 && shouldRenderSideBarSection(domain, 'data-products');
|
|
23
|
+
|
|
21
24
|
const subDomains = domain.data.domains || [];
|
|
22
25
|
const renderSubDomains = subDomains.length > 0 && shouldRenderSideBarSection(domain, 'subdomains');
|
|
23
26
|
|
|
@@ -157,6 +160,14 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
|
|
|
157
160
|
icon: 'Server',
|
|
158
161
|
pages: servicesInDomain.map((service) => `service:${(service as any).data.id}:${(service as any).data.version}`),
|
|
159
162
|
},
|
|
163
|
+
renderDataProducts && {
|
|
164
|
+
type: 'group',
|
|
165
|
+
title: 'Data Products',
|
|
166
|
+
icon: 'Package',
|
|
167
|
+
pages: dataProductsInDomain.map(
|
|
168
|
+
(dataProduct) => `data-product:${(dataProduct as any).data.id}:${(dataProduct as any).data.version}`
|
|
169
|
+
),
|
|
170
|
+
},
|
|
160
171
|
sendsMessages.length > 0 &&
|
|
161
172
|
renderMessages && {
|
|
162
173
|
type: 'group',
|