@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.
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-O6SRHGZ7.js → chunk-4EJDLNIX.js} +1 -1
  6. package/dist/{chunk-WAX3S32H.js → chunk-EG36OTR7.js} +1 -1
  7. package/dist/{chunk-GQZVIS3Z.js → chunk-GITARDPK.js} +1 -1
  8. package/dist/{chunk-7CTNGTBB.js → chunk-IEEU454Z.js} +1 -1
  9. package/dist/{chunk-M7EPRGHR.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 +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
@@ -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
 
@@ -22,6 +22,7 @@ export class Page extends HybridPage {
22
22
  'channels',
23
23
  'entities',
24
24
  'containers',
25
+ 'data-products',
25
26
  ];
26
27
  const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]()));
27
28
 
@@ -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
- // return allItems.flatMap((items, index) =>
32
- return itemsWithSchema.map((item, index) => ({
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 fs from 'fs';
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
- const allItems = await pageDataLoader[type]();
22
- const versions = allItems.filter((item) => item.data.id === data.id);
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 schemaPath = message.data.schemaPath;
32
- const fullSchemaPath = path.join(path.dirname(message.filePath ?? ''), schemaPath ?? '');
45
+ const schemaContent = readResourceFile(dataProduct, contractPath) ?? '';
46
+ const schemaExtension = path.extname(contractPath).slice(1);
33
47
 
34
- let schemaContent = '';
35
- if (fs.existsSync(fullSchemaPath)) {
36
- schemaContent = fs.readFileSync(fullSchemaPath, 'utf-8');
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
- const schemaExtension = path.extname(schemaPath ?? '').slice(1);
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
- // Sort versions descending (newest first)
67
- const sortedVersions = sortVersioned(validVersions, (item) => item.data.version);
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-gray-500">Schema not found or could not be loaded.</div>
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
- return [...messagesWithSchemas, ...flatServicesWithSpecs, ...flatDomainsWithSpecs];
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[] = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'containers'];
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
- const renderServicesWritingToContainer =
23
- servicesWritingToContainer.length > 0 && shouldRenderSideBarSection(container, 'services');
24
- const renderServicesReadingFromContainer =
25
- servicesReadingFromContainer.length > 0 && shouldRenderSideBarSection(container, 'services');
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
- renderServicesWritingToContainer && {
81
+ renderWrites && {
71
82
  type: 'group',
72
- title: 'Services (Writes)',
73
- icon: 'Server',
74
- pages: servicesWritingToContainer.map(
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
- renderServicesReadingFromContainer && {
87
+ renderReads && {
79
88
  type: 'group',
80
- title: 'Services (Reads)',
81
- icon: 'Server',
82
- pages: servicesReadingFromContainer.map(
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',