@eventcatalog/core 3.7.2 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-M7EPRGHR.js → chunk-4BEERWPE.js} +1 -1
  6. package/dist/{chunk-GQZVIS3Z.js → chunk-NV7H3356.js} +1 -1
  7. package/dist/{chunk-7CTNGTBB.js → chunk-POQENB7N.js} +1 -1
  8. package/dist/{chunk-WAX3S32H.js → chunk-SL2YYN6D.js} +1 -1
  9. package/dist/{chunk-O6SRHGZ7.js → chunk-ZBC6HM3V.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +13 -1
  19. package/eventcatalog/src/components/Grids/DomainGrid.tsx +109 -6
  20. package/eventcatalog/src/components/Grids/utils.tsx +10 -1
  21. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -0
  22. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +4 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +132 -0
  24. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +29 -2
  25. package/eventcatalog/src/components/SchemaExplorer/types.ts +5 -1
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +3 -0
  27. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +1 -0
  28. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +23 -1
  29. package/eventcatalog/src/components/Tables/Discover/columns.tsx +62 -0
  30. package/eventcatalog/src/content.config.ts +34 -0
  31. package/eventcatalog/src/enterprise/ai/chat-api.ts +26 -0
  32. package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +1 -1
  33. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +169 -2
  34. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -1
  35. package/eventcatalog/src/pages/discover/[type]/index.astro +57 -1
  36. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
  37. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -1
  38. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +27 -3
  39. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +74 -25
  40. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +55 -1
  41. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +10 -1
  42. package/eventcatalog/src/stores/sidebar-store/builders/container.ts +23 -16
  43. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +130 -0
  44. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +11 -0
  45. package/eventcatalog/src/stores/sidebar-store/state.ts +68 -13
  46. package/eventcatalog/src/styles/theme.css +4 -0
  47. package/eventcatalog/src/styles/themes/forest.css +4 -0
  48. package/eventcatalog/src/styles/themes/ocean.css +4 -0
  49. package/eventcatalog/src/styles/themes/sapphire.css +4 -0
  50. package/eventcatalog/src/styles/themes/sunset.css +4 -0
  51. package/eventcatalog/src/types/index.ts +4 -2
  52. package/eventcatalog/src/utils/collections/commands.ts +11 -29
  53. package/eventcatalog/src/utils/collections/containers.ts +25 -1
  54. package/eventcatalog/src/utils/collections/data-products.ts +85 -0
  55. package/eventcatalog/src/utils/collections/domains.ts +28 -10
  56. package/eventcatalog/src/utils/collections/events.ts +11 -29
  57. package/eventcatalog/src/utils/collections/icons.ts +5 -0
  58. package/eventcatalog/src/utils/collections/messages.ts +68 -0
  59. package/eventcatalog/src/utils/collections/queries.ts +11 -29
  60. package/eventcatalog/src/utils/collections/util.ts +11 -2
  61. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +91 -3
  62. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +225 -0
  63. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +28 -2
  64. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +74 -20
  65. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  66. package/package.json +2 -2
@@ -5,6 +5,7 @@ import {
5
5
  ChatBubbleLeftIcon,
6
6
  MagnifyingGlassIcon as MagnifyingGlassSolidIcon,
7
7
  CodeBracketIcon,
8
+ DocumentCheckIcon,
8
9
  } from '@heroicons/react/24/solid';
9
10
  import type { CollectionMessageTypes } from '@types';
10
11
 
@@ -30,7 +31,7 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
30
31
  }
31
32
  return '';
32
33
  });
33
- const [selectedTypes, setSelectedTypes] = useState<Set<CollectionMessageTypes | 'specifications'>>(() => {
34
+ const [selectedTypes, setSelectedTypes] = useState<Set<CollectionMessageTypes | 'specifications' | 'data-contracts'>>(() => {
34
35
  // Load from localStorage
35
36
  if (typeof window !== 'undefined') {
36
37
  const stored = localStorage.getItem('schemaRegistrySelectedTypes');
@@ -151,6 +152,10 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
151
152
  if (selectedTypes.has('specifications') && SPEC_TYPES.includes(msg.schemaExtension?.toLowerCase() || '')) {
152
153
  return true;
153
154
  }
155
+ // Check if 'data-contracts' is selected and this is a data product contract
156
+ if (selectedTypes.has('data-contracts') && msg.collection === 'data-products') {
157
+ return true;
158
+ }
154
159
  return false;
155
160
  });
156
161
  }
@@ -325,11 +330,12 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
325
330
  commands: latestMessages.filter((m) => m.collection === 'commands').length,
326
331
  queries: latestMessages.filter((m) => m.collection === 'queries').length,
327
332
  specifications: latestMessages.filter((m) => SPEC_TYPES.includes(m.schemaExtension?.toLowerCase() || '')).length,
333
+ dataContracts: latestMessages.filter((m) => m.collection === 'data-products').length,
328
334
  };
329
335
  }, [latestMessages]);
330
336
 
331
337
  // Toggle type selection (multi-select)
332
- const toggleType = (type: CollectionMessageTypes | 'specifications') => {
338
+ const toggleType = (type: CollectionMessageTypes | 'specifications' | 'data-contracts') => {
333
339
  setSelectedTypes((prev) => {
334
340
  const next = new Set(prev);
335
341
  if (next.has(type)) {
@@ -485,6 +491,27 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
485
491
  </span>
486
492
  </button>
487
493
  )}
494
+ {stats.dataContracts > 0 && (
495
+ <button
496
+ onClick={() => toggleType('data-contracts')}
497
+ className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium transition-all border ${
498
+ selectedTypes.has('data-contracts')
499
+ ? 'bg-purple-50 dark:bg-purple-500/10 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-500/30'
500
+ : 'text-[rgb(var(--ec-page-text-muted))] border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-content-hover))]'
501
+ }`}
502
+ title="Data Contracts from Data Products"
503
+ >
504
+ <DocumentCheckIcon
505
+ className={`h-3.5 w-3.5 ${selectedTypes.has('data-contracts') ? 'text-purple-500' : 'text-purple-400'}`}
506
+ />
507
+ <span>Data Contracts</span>
508
+ <span
509
+ className={`tabular-nums ${selectedTypes.has('data-contracts') ? 'text-purple-500' : 'text-[rgb(var(--ec-icon-color))]'}`}
510
+ >
511
+ {stats.dataContracts}
512
+ </span>
513
+ </button>
514
+ )}
488
515
  </div>
489
516
  </div>
490
517
 
@@ -18,7 +18,7 @@ export interface Owner {
18
18
  }
19
19
 
20
20
  export interface SchemaItem {
21
- collection: CollectionMessageTypes | 'services';
21
+ collection: CollectionMessageTypes | 'services' | 'data-products';
22
22
  data: {
23
23
  id: string;
24
24
  name: string;
@@ -34,6 +34,10 @@ export interface SchemaItem {
34
34
  specType?: string;
35
35
  specName?: string;
36
36
  specFilenameWithoutExtension?: string;
37
+ // For data contracts
38
+ contractType?: string;
39
+ dataProductId?: string;
40
+ dataProductVersion?: string;
37
41
  }
38
42
 
39
43
  export interface VersionDiff {
@@ -222,6 +222,9 @@ export default function NestedSideBar() {
222
222
  // Flows
223
223
  { pattern: /^\/docs\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
224
224
  { pattern: /^\/visualiser\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
225
+ // Data Products
226
+ { pattern: /^\/docs\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
227
+ { pattern: /^\/visualiser\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
225
228
  ];
226
229
 
227
230
  // URL patterns without version (language pages, etc)
@@ -14,6 +14,7 @@ export const getBadgeClasses = (badge: string): string => {
14
14
  message: 'bg-[rgb(var(--ec-badge-message-bg))] text-[rgb(var(--ec-badge-message-text))]',
15
15
  design: 'bg-[rgb(var(--ec-badge-design-bg))] text-[rgb(var(--ec-badge-design-text))]',
16
16
  channel: 'bg-[rgb(var(--ec-badge-channel-bg))] text-[rgb(var(--ec-badge-channel-text))]',
17
+ 'data product': 'bg-[rgb(var(--ec-badge-data-product-bg))] text-[rgb(var(--ec-badge-data-product-text))]',
17
18
  };
18
19
  return badgeColors[badge.toLowerCase()] || 'bg-[rgb(var(--ec-badge-default-bg))] text-[rgb(var(--ec-badge-default-text))]';
19
20
  };
@@ -18,7 +18,15 @@ import { FilterDropdown, CheckboxItem } from './FilterComponents';
18
18
  import DebouncedInput from '../DebouncedInput';
19
19
  import { getDiscoverColumns } from './columns';
20
20
 
21
- export type CollectionType = 'events' | 'commands' | 'queries' | 'services' | 'domains' | 'flows' | 'containers';
21
+ export type CollectionType =
22
+ | 'events'
23
+ | 'commands'
24
+ | 'queries'
25
+ | 'services'
26
+ | 'domains'
27
+ | 'flows'
28
+ | 'containers'
29
+ | 'data-products';
22
30
 
23
31
  export interface DiscoverTableData {
24
32
  collection: string;
@@ -29,6 +37,8 @@ export interface DiscoverTableData {
29
37
  hasRepository?: boolean;
30
38
  isDeprecated?: boolean;
31
39
  hasDataDependencies?: boolean;
40
+ hasInputs?: boolean;
41
+ hasOutputs?: boolean;
32
42
  data: {
33
43
  id: string;
34
44
  name: string;
@@ -49,6 +59,8 @@ export interface DiscoverTableData {
49
59
  services?: Array<any>;
50
60
  servicesThatWriteToContainer?: Array<any>;
51
61
  servicesThatReadFromContainer?: Array<any>;
62
+ inputs?: Array<any>;
63
+ outputs?: Array<any>;
52
64
  };
53
65
  }
54
66
 
@@ -232,6 +244,16 @@ export function DiscoverTable<T extends DiscoverTableData>({
232
244
  const readers = row.data.servicesThatReadFromContainer || [];
233
245
  if (readers.length === 0) return false;
234
246
  }
247
+
248
+ // Data-product-specific checks
249
+ if (prop === 'hasInputs') {
250
+ const inputs = row.data.inputs || [];
251
+ if (inputs.length === 0) return false;
252
+ }
253
+ if (prop === 'hasOutputs') {
254
+ const outputs = row.data.outputs || [];
255
+ if (outputs.length === 0) return false;
256
+ }
235
257
  }
236
258
  }
237
259
 
@@ -9,6 +9,7 @@ import {
9
9
  QueueListIcon,
10
10
  DocumentTextIcon,
11
11
  MapIcon,
12
+ CubeIcon,
12
13
  } from '@heroicons/react/24/solid';
13
14
  import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/24/outline';
14
15
  import { DatabaseIcon } from 'lucide-react';
@@ -30,6 +31,7 @@ const colorClasses: Record<string, string> = {
30
31
  purple: 'text-purple-500',
31
32
  red: 'text-red-500',
32
33
  gray: 'text-gray-500',
34
+ cyan: 'text-cyan-500',
33
35
  };
34
36
 
35
37
  // Reusable tooltip wrapper component
@@ -540,6 +542,64 @@ export const getContainerColumns = (tableConfiguration: TableConfiguration) => [
540
542
  createActionsColumn('containers', tableConfiguration),
541
543
  ];
542
544
 
545
+ // ============================================================================
546
+ // DATA PRODUCT COLUMNS
547
+ // ============================================================================
548
+ export const getDataProductColumns = (tableConfiguration: TableConfiguration) => [
549
+ columnHelper.accessor('data.name', {
550
+ id: 'name',
551
+ header: () => <span>{tableConfiguration?.columns?.name?.label || 'Data Product'}</span>,
552
+ cell: (info) => {
553
+ const item = info.row.original;
554
+ const isLatestVersion = item.data.version === item.data.latestVersion;
555
+ return (
556
+ <a
557
+ href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
558
+ className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
559
+ >
560
+ <CubeIcon className="h-4 w-4 text-cyan-500 flex-shrink-0" />
561
+ <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
562
+ {item.data.name}
563
+ </span>
564
+ {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
565
+ </a>
566
+ );
567
+ },
568
+ meta: {
569
+ filterVariant: 'name',
570
+ },
571
+ }),
572
+ createSummaryColumn(tableConfiguration),
573
+ columnHelper.accessor('data.inputs', {
574
+ id: 'inputs',
575
+ header: () => (
576
+ <span className="flex items-center gap-1">
577
+ <ArrowDownIcon className="w-3.5 h-3.5" />
578
+ Inputs
579
+ </span>
580
+ ),
581
+ cell: (info) => <CollectionListCell items={info.getValue()} />,
582
+ meta: {
583
+ showFilter: false,
584
+ },
585
+ }),
586
+ columnHelper.accessor('data.outputs', {
587
+ id: 'outputs',
588
+ header: () => (
589
+ <span className="flex items-center gap-1">
590
+ <ArrowUpIcon className="w-3.5 h-3.5" />
591
+ Outputs
592
+ </span>
593
+ ),
594
+ cell: (info) => <CollectionListCell items={info.getValue()} />,
595
+ meta: {
596
+ showFilter: false,
597
+ },
598
+ }),
599
+ createBadgesColumn(tableConfiguration),
600
+ createActionsColumn('data-products', tableConfiguration),
601
+ ];
602
+
543
603
  // ============================================================================
544
604
  // COLUMN GETTER BY COLLECTION TYPE
545
605
  // ============================================================================
@@ -559,6 +619,8 @@ export const getDiscoverColumns = (collectionType: CollectionType, tableConfigur
559
619
  return getFlowColumns(tableConfiguration);
560
620
  case 'containers':
561
621
  return getContainerColumns(tableConfiguration);
622
+ case 'data-products':
623
+ return getDataProductColumns(tableConfiguration);
562
624
  default:
563
625
  return [];
564
626
  }
@@ -362,6 +362,34 @@ const queries = defineCollection({
362
362
  .merge(baseSchema),
363
363
  });
364
364
 
365
+ const dataProductOutputPointer = z.object({
366
+ id: z.string(),
367
+ version: z.string().optional().default('latest'),
368
+ contract: z
369
+ .object({
370
+ path: z.string(),
371
+ name: z.string(),
372
+ type: z.string().optional(),
373
+ })
374
+ .optional(),
375
+ });
376
+
377
+ const dataProducts = defineCollection({
378
+ loader: glob({
379
+ pattern: ['**/data-products/*/index.(md|mdx)', '**/data-products/*/versioned/*/index.(md|mdx)'],
380
+ base: projectDirBase,
381
+ generateId: ({ data }) => {
382
+ return `${data.id}-${data.version}`;
383
+ },
384
+ }),
385
+ schema: z
386
+ .object({
387
+ inputs: z.array(pointer).optional(),
388
+ outputs: z.array(dataProductOutputPointer).optional(),
389
+ })
390
+ .merge(baseSchema),
391
+ });
392
+
365
393
  const services = defineCollection({
366
394
  loader: glob({
367
395
  pattern: [
@@ -455,6 +483,8 @@ const containers = defineCollection({
455
483
 
456
484
  servicesThatWriteToContainer: z.array(reference('services')).optional(),
457
485
  servicesThatReadFromContainer: z.array(reference('services')).optional(),
486
+ dataProductsThatWriteToContainer: z.array(reference('data-products')).optional(),
487
+ dataProductsThatReadFromContainer: z.array(reference('data-products')).optional(),
458
488
  })
459
489
  .merge(baseSchema),
460
490
  });
@@ -489,6 +519,7 @@ const domains = defineCollection({
489
519
  services: z.array(pointer).optional(),
490
520
  domains: z.array(pointer).optional(),
491
521
  entities: z.array(pointer).optional(),
522
+ 'data-products': z.array(pointer).optional(),
492
523
  flows: z.array(pointer).optional(),
493
524
  sends: z.array(sendsPointer).optional(),
494
525
  receives: z.array(receivesPointer).optional(),
@@ -725,6 +756,9 @@ export const collections = {
725
756
  changelogs,
726
757
  containers,
727
758
 
759
+ // Data Product Collections
760
+ 'data-products': dataProducts,
761
+
728
762
  // DDD Collections
729
763
  ubiquitousLanguages,
730
764
  entities,
@@ -9,6 +9,8 @@ import {
9
9
  getResource as getResourceImpl,
10
10
  getMessagesProducedOrConsumedByResource as getMessagesImpl,
11
11
  getSchemaForResource as getSchemaImpl,
12
+ getDataProductInputs as getDataProductInputsImpl,
13
+ getDataProductOutputs as getDataProductOutputsImpl,
12
14
  collectionSchema,
13
15
  resourceCollectionSchema,
14
16
  messageCollectionSchema,
@@ -57,6 +59,8 @@ const builtInToolsMetadata = [
57
59
  { name: 'getProducerAndConsumerForMessage', description: 'Get the producers and consumers for a message' },
58
60
  { name: 'getConsumersOfMessage', description: 'Get the consumers for a message' },
59
61
  { name: 'getSchemaForResource', description: 'Get the schema or specifications (OpenAPI, AsyncAPI, GraphQL) for a resource' },
62
+ { name: 'getDataProductInputs', description: 'Get the inputs (resources consumed) for a data product' },
63
+ { name: 'getDataProductOutputs', description: 'Get the outputs (resources produced) for a data product with data contracts' },
60
64
  ];
61
65
 
62
66
  // Get extended tools metadata from user configuration
@@ -94,6 +98,8 @@ There are many different resource types in EventCatalog, including:
94
98
  - example docs url: /docs/entities/MyEntity/1.0.0
95
99
  - Containers (collection name 'containers') (at the moment these are data stores (databases))
96
100
  - example docs url: /docs/containers/MyContainer/1.0.0
101
+ - Data Products (collection name 'data-products') (data products that have inputs and outputs, and may have data contracts)
102
+ - example docs url: /docs/data-products/MyDataProduct/1.0.0
97
103
 
98
104
  The user will ask you some questions about the software architecture catalog, you should use the tools provided to you to get the information they need.
99
105
 
@@ -291,6 +297,26 @@ export const POST = async ({ request }: APIContext<{ question: string; messages:
291
297
  return await getSchemaImpl({ resourceId, resourceVersion, resourceCollection });
292
298
  },
293
299
  }),
300
+ getDataProductInputs: tool({
301
+ description: toolDescriptions.getDataProductInputs,
302
+ inputSchema: z.object({
303
+ dataProductId: z.string().describe('The id of the data product to get the inputs for'),
304
+ dataProductVersion: z.string().describe('The version of the data product to get the inputs for'),
305
+ }),
306
+ execute: async ({ dataProductId, dataProductVersion }) => {
307
+ return await getDataProductInputsImpl({ dataProductId, dataProductVersion });
308
+ },
309
+ }),
310
+ getDataProductOutputs: tool({
311
+ description: toolDescriptions.getDataProductOutputs,
312
+ inputSchema: z.object({
313
+ dataProductId: z.string().describe('The id of the data product to get the outputs for'),
314
+ dataProductVersion: z.string().describe('The version of the data product to get the outputs for'),
315
+ }),
316
+ execute: async ({ dataProductId, dataProductVersion }) => {
317
+ return await getDataProductOutputsImpl({ dataProductId, dataProductVersion });
318
+ },
319
+ }),
294
320
  suggestFollowUpQuestions: tool({
295
321
  description:
296
322
  'Use this tool after answering a question to suggest 2-3 relevant follow-up questions the user might want to ask. These will be displayed as clickable suggestions.',
@@ -210,6 +210,6 @@ export const getAdjacentPages = async (slug: string): Promise<AdjacentPages> =>
210
210
  };
211
211
 
212
212
  export const getNavigationItems = async (): Promise<SidebarItem[]> => {
213
- const configuredSidebar = config.customDocs.sidebar;
213
+ const configuredSidebar = config.customDocs?.sidebar || [];
214
214
  return processSidebarItems(configuredSidebar as SideBarConfigurationItem[]);
215
215
  };
@@ -4,9 +4,10 @@
4
4
  */
5
5
  import { getCollection, getEntry } from 'astro:content';
6
6
  import { z } from 'zod';
7
- import { getSchemasFromResource } from '@utils/collections/schemas';
7
+ import { getSchemasFromResource, getSchemaFormatFromURL } from '@utils/collections/schemas';
8
8
  import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
9
9
  import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
10
+ import { getAbsoluteFilePathForAstroFile } from '@utils/files';
10
11
  import fs from 'node:fs';
11
12
 
12
13
  // ============================================
@@ -92,6 +93,7 @@ export const collectionSchema = z.enum([
92
93
  'entities',
93
94
  'containers',
94
95
  'diagrams',
96
+ 'data-products',
95
97
  ]);
96
98
 
97
99
  export const messageCollectionSchema = z.enum(['events', 'commands', 'queries']);
@@ -105,6 +107,7 @@ export const resourceCollectionSchema = z.enum([
105
107
  'domains',
106
108
  'channels',
107
109
  'entities',
110
+ 'data-products',
108
111
  ]);
109
112
 
110
113
  // ============================================
@@ -208,7 +211,17 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
208
211
  * Find all resources owned by a specific team or user
209
212
  */
210
213
  export async function findResourcesByOwner(params: { ownerId: string }) {
211
- const collectionsToSearch = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'channels', 'entities'] as const;
214
+ const collectionsToSearch = [
215
+ 'events',
216
+ 'commands',
217
+ 'queries',
218
+ 'services',
219
+ 'domains',
220
+ 'flows',
221
+ 'channels',
222
+ 'entities',
223
+ 'data-products',
224
+ ] as const;
212
225
 
213
226
  const results: Array<{ collection: string; id: string; version?: string; name: string }> = [];
214
227
 
@@ -658,6 +671,156 @@ export async function explainUbiquitousLanguageTerms(params: { domainId: string;
658
671
  };
659
672
  }
660
673
 
674
+ // ============================================
675
+ // Data Product tools
676
+ // ============================================
677
+
678
+ /**
679
+ * Get the inputs (resources consumed) for a data product
680
+ * Returns fully hydrated input resources
681
+ */
682
+ export async function getDataProductInputs(params: { dataProductId: string; dataProductVersion: string }) {
683
+ const dataProduct = await getEntry('data-products', `${params.dataProductId}-${params.dataProductVersion}`);
684
+
685
+ if (!dataProduct) {
686
+ return {
687
+ error: `Data product not found: ${params.dataProductId}-${params.dataProductVersion}`,
688
+ };
689
+ }
690
+
691
+ const inputPointers = (dataProduct.data as any).inputs || [];
692
+
693
+ if (inputPointers.length === 0) {
694
+ return {
695
+ dataProductId: params.dataProductId,
696
+ dataProductVersion: params.dataProductVersion,
697
+ message: 'No inputs found for this data product',
698
+ inputs: [],
699
+ };
700
+ }
701
+
702
+ // Fetch all collections that can be inputs (messages, channels, containers, services)
703
+ const [allEvents, allCommands, allQueries, allChannels, allContainers, allServices] = await Promise.all([
704
+ getCollection('events'),
705
+ getCollection('commands'),
706
+ getCollection('queries'),
707
+ getCollection('channels'),
708
+ getCollection('containers'),
709
+ getCollection('services'),
710
+ ]);
711
+
712
+ const allResources = [...allEvents, ...allCommands, ...allQueries, ...allChannels, ...allContainers, ...allServices];
713
+
714
+ // Hydrate inputs by finding matching resources
715
+ const hydratedInputs = inputPointers
716
+ .map((pointer: { id: string; version?: string }) => {
717
+ const matches = getItemsFromCollectionByIdAndSemverOrLatest(allResources, pointer.id, pointer.version);
718
+ const resource = matches[0];
719
+ if (!resource) return null;
720
+
721
+ return {
722
+ id: (resource.data as any).id,
723
+ version: (resource.data as any).version,
724
+ name: (resource.data as any).name || (resource.data as any).id,
725
+ summary: (resource.data as any).summary,
726
+ collection: resource.collection,
727
+ };
728
+ })
729
+ .filter(Boolean);
730
+
731
+ return {
732
+ dataProductId: params.dataProductId,
733
+ dataProductVersion: params.dataProductVersion,
734
+ dataProductName: (dataProduct.data as any).name || params.dataProductId,
735
+ inputs: hydratedInputs,
736
+ totalCount: hydratedInputs.length,
737
+ };
738
+ }
739
+
740
+ /**
741
+ * Get the outputs (resources produced) for a data product
742
+ * Returns fully hydrated output resources with their contracts
743
+ */
744
+ export async function getDataProductOutputs(params: { dataProductId: string; dataProductVersion: string }) {
745
+ const dataProduct = await getEntry('data-products', `${params.dataProductId}-${params.dataProductVersion}`);
746
+
747
+ if (!dataProduct) {
748
+ return {
749
+ error: `Data product not found: ${params.dataProductId}-${params.dataProductVersion}`,
750
+ };
751
+ }
752
+
753
+ const outputPointers = (dataProduct.data as any).outputs || [];
754
+
755
+ if (outputPointers.length === 0) {
756
+ return {
757
+ dataProductId: params.dataProductId,
758
+ dataProductVersion: params.dataProductVersion,
759
+ message: 'No outputs found for this data product',
760
+ outputs: [],
761
+ };
762
+ }
763
+
764
+ // Fetch all collections that can be outputs (messages, channels, containers, services)
765
+ const [allEvents, allCommands, allQueries, allChannels, allContainers, allServices] = await Promise.all([
766
+ getCollection('events'),
767
+ getCollection('commands'),
768
+ getCollection('queries'),
769
+ getCollection('channels'),
770
+ getCollection('containers'),
771
+ getCollection('services'),
772
+ ]);
773
+
774
+ const allResources = [...allEvents, ...allCommands, ...allQueries, ...allChannels, ...allContainers, ...allServices];
775
+
776
+ // Hydrate outputs by finding matching resources and including contract info
777
+ const hydratedOutputs = outputPointers
778
+ .map((pointer: { id: string; version?: string; contract?: { path: string; name: string; type?: string } }) => {
779
+ const matches = getItemsFromCollectionByIdAndSemverOrLatest(allResources, pointer.id, pointer.version);
780
+ const resource = matches[0];
781
+ if (!resource) return null;
782
+
783
+ const result: any = {
784
+ id: (resource.data as any).id,
785
+ version: (resource.data as any).version,
786
+ name: (resource.data as any).name || (resource.data as any).id,
787
+ summary: (resource.data as any).summary,
788
+ collection: resource.collection,
789
+ };
790
+
791
+ // Include contract information if present
792
+ if (pointer.contract) {
793
+ const absoluteContractPath = getAbsoluteFilePathForAstroFile(dataProduct.filePath ?? '', pointer.contract.path);
794
+ let contractContent: string | null = null;
795
+
796
+ try {
797
+ contractContent = fs.readFileSync(absoluteContractPath, 'utf-8');
798
+ } catch {
799
+ // File may not be accessible
800
+ }
801
+
802
+ result.contract = {
803
+ name: pointer.contract.name,
804
+ path: pointer.contract.path,
805
+ format: getSchemaFormatFromURL(pointer.contract.path),
806
+ type: pointer.contract.type,
807
+ content: contractContent,
808
+ };
809
+ }
810
+
811
+ return result;
812
+ })
813
+ .filter(Boolean);
814
+
815
+ return {
816
+ dataProductId: params.dataProductId,
817
+ dataProductVersion: params.dataProductVersion,
818
+ dataProductName: (dataProduct.data as any).name || params.dataProductId,
819
+ outputs: hydratedOutputs,
820
+ totalCount: hydratedOutputs.length,
821
+ };
822
+ }
823
+
661
824
  // ============================================
662
825
  // Tool metadata (descriptions)
663
826
  // ============================================
@@ -687,4 +850,8 @@ export const toolDescriptions = {
687
850
  'Use this tool when a user shares a schema file (Avro, JSON Schema, Protobuf) and wants to find it in EventCatalog. Look for "x-eventcatalog-id" and "x-eventcatalog-version" in the schema - these may be properties in the schema OR in comments (e.g. // x-eventcatalog-id: OrderCreated). Pass the id as messageId. If version exists, pass it as messageVersion, otherwise omit it to get the latest version. Returns the message resource along with its producers and consumers.',
688
851
  explainUbiquitousLanguageTerms:
689
852
  'Use this tool to explain ubiquitous language terms from Domain-Driven Design for a specific domain. Returns the glossary of terms defined for the domain and its subdomains, including duplicate term detection.',
853
+ getDataProductInputs:
854
+ 'Use this tool to get the inputs (resources consumed) for a data product. Returns fully hydrated input resources with their id, version, name, summary, and collection type.',
855
+ getDataProductOutputs:
856
+ 'Use this tool to get the outputs (resources produced) for a data product. Returns fully hydrated output resources with their id, version, name, summary, collection type, and data contracts (if defined). Data contracts include the contract name, path, format, type, and content.',
690
857
  };
@@ -11,14 +11,16 @@ export class Page extends HybridPage {
11
11
  static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
12
12
  const { getFlows } = await import('@utils/collections/flows');
13
13
  const { getServices } = await import('@utils/collections/services');
14
+ const { getDataProducts } = await import('@utils/collections/data-products');
14
15
 
15
16
  const loaders = {
16
17
  ...pageDataLoader,
17
18
  flows: getFlows,
18
19
  services: getServices,
20
+ 'data-products': getDataProducts,
19
21
  };
20
22
 
21
- const itemTypes = ['events', 'commands', 'queries', 'domains', 'services', 'flows', 'containers'] as const;
23
+ const itemTypes = ['events', 'commands', 'queries', 'domains', 'services', 'flows', 'containers', 'data-products'] as const;
22
24
  const allItems = await Promise.all(itemTypes.map((type) => loaders[type]()));
23
25
 
24
26
  return allItems.flatMap((items, index) => ({
@@ -41,11 +43,13 @@ export class Page extends HybridPage {
41
43
 
42
44
  const { getFlows } = await import('@utils/collections/flows');
43
45
  const { getServices } = await import('@utils/collections/services');
46
+ const { getDataProducts } = await import('@utils/collections/data-products');
44
47
 
45
48
  const loaders = {
46
49
  ...pageDataLoader,
47
50
  flows: getFlows,
48
51
  services: getServices,
52
+ 'data-products': getDataProducts,
49
53
  };
50
54
 
51
55
  // @ts-ignore