@eventcatalog/core 3.44.2 → 3.46.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 (39) 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-THDZCUFV.js → chunk-DOHA5HNJ.js} +1 -1
  6. package/dist/{chunk-BLJ5FRR3.js → chunk-FNOYJEUK.js} +1 -1
  7. package/dist/{chunk-LIIOK6SZ.js → chunk-JS6IYB55.js} +1 -1
  8. package/dist/{chunk-IU3D7JRW.js → chunk-TLLUDBO4.js} +1 -1
  9. package/dist/{chunk-RUACTEJR.js → chunk-X4AESI6E.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.config.d.cts +29 -0
  14. package/dist/eventcatalog.config.d.ts +29 -0
  15. package/dist/eventcatalog.js +10 -10
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/utils/cli-logger.cjs +1 -1
  19. package/dist/utils/cli-logger.js +2 -2
  20. package/eventcatalog/astro.config.mjs +7 -10
  21. package/eventcatalog/ec.config.mjs +20 -0
  22. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerPortal.tsx +1 -1
  23. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +19 -47
  24. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +53 -0
  25. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.ts +133 -0
  26. package/eventcatalog/src/content.config.ts +65 -0
  27. package/eventcatalog/src/enterprise/api/schemas/[collection]/[id]/[version]/index.ts +42 -50
  28. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +23 -0
  29. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -1
  30. package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +25 -17
  31. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +33 -16
  32. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +18 -8
  33. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +45 -21
  34. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +25 -2
  35. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -0
  36. package/eventcatalog/src/stores/sidebar-store/state.ts +3 -0
  37. package/eventcatalog/src/utils/collections/schema-loader.ts +523 -0
  38. package/eventcatalog/src/utils/files.ts +1 -1
  39. package/package.json +4 -4
@@ -9,6 +9,7 @@ import fs from 'fs';
9
9
  import path from 'path';
10
10
  import { userTeamDirectoryLoader } from './enterprise/directory/user-team-directory';
11
11
  import config from '@config';
12
+ import { schemaLoader } from './utils/collections/schema-loader';
12
13
 
13
14
  // Enterprise Collections
14
15
  import { customPagesSchema, resourceDocsSchema, resourceDocCategoriesSchema } from './enterprise/collections';
@@ -68,6 +69,17 @@ const channelPointer = z
68
69
  })
69
70
  .extend(pointer.shape);
70
71
 
72
+ const schemaPointer = z.object({
73
+ id: z.string().optional(),
74
+ ref: z.string().optional(),
75
+ file: z.string().optional(),
76
+ path: z.string().optional(),
77
+ name: z.string().optional(),
78
+ format: z.string().optional(),
79
+ environments: z.array(z.string()).optional(),
80
+ default: z.boolean().optional(),
81
+ });
82
+
71
83
  const sendsPointer = z.object({
72
84
  id: z.string(),
73
85
  version: z.string().optional().default('latest'),
@@ -375,6 +387,7 @@ const events = defineCollection({
375
387
  producers: z.array(reference('services')).optional(),
376
388
  consumers: z.array(reference('services')).optional(),
377
389
  channels: z.array(channelPointer).optional(),
390
+ schemas: z.array(schemaPointer).optional(),
378
391
  messageChannels: z.array(reference('channels')).optional(),
379
392
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
380
393
  })
@@ -397,6 +410,7 @@ const commands = defineCollection({
397
410
  producers: z.array(reference('services')).optional(),
398
411
  consumers: z.array(reference('services')).optional(),
399
412
  channels: z.array(channelPointer).optional(),
413
+ schemas: z.array(schemaPointer).optional(),
400
414
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
401
415
  messageChannels: z.array(reference('channels')).optional(),
402
416
  })
@@ -419,6 +433,7 @@ const queries = defineCollection({
419
433
  producers: z.array(reference('services')).optional(),
420
434
  consumers: z.array(reference('services')).optional(),
421
435
  channels: z.array(channelPointer).optional(),
436
+ schemas: z.array(schemaPointer).optional(),
422
437
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
423
438
  messageChannels: z.array(reference('channels')).optional(),
424
439
  })
@@ -991,6 +1006,53 @@ const diagrams = defineCollection({
991
1006
  .extend(baseSchema.shape),
992
1007
  });
993
1008
 
1009
+ const schemas = defineCollection({
1010
+ loader: schemaLoader({
1011
+ messages: {
1012
+ pattern: withIgnoredBuildArtifacts([
1013
+ '**/events/*/index.{md,mdx}',
1014
+ '**/events/*/versioned/*/index.{md,mdx}',
1015
+ '**/commands/*/index.{md,mdx}',
1016
+ '**/commands/*/versioned/*/index.{md,mdx}',
1017
+ '**/queries/*/index.{md,mdx}',
1018
+ '**/queries/*/versioned/*/index.{md,mdx}',
1019
+ ]) as string[],
1020
+ base: projectDirBase,
1021
+ },
1022
+ sources: config.schemas?.sources ?? [],
1023
+ }),
1024
+ schema: z
1025
+ .object({
1026
+ format: z.string(),
1027
+ schemaId: z.string().optional(),
1028
+ ref: z.string().optional(),
1029
+ content: z.string().optional(),
1030
+ file: z.string().optional(),
1031
+ filePath: z.string().optional(),
1032
+ environments: z.array(z.string()).optional(),
1033
+ default: z.boolean().optional(),
1034
+ latest: z.boolean().optional(),
1035
+ message: z.object({
1036
+ collection: z.enum(['events', 'commands', 'queries']),
1037
+ id: z.string(),
1038
+ name: z.string().optional(),
1039
+ version: z.string(),
1040
+ summary: z.string().optional(),
1041
+ owners: z.array(z.string()).optional(),
1042
+ }),
1043
+ source: z.object({
1044
+ provider: z.string(),
1045
+ id: z.string().optional(),
1046
+ path: z.string().optional(),
1047
+ url: z.string().optional(),
1048
+ ref: z.string().optional(),
1049
+ branch: z.string().optional(),
1050
+ }),
1051
+ readOnly: z.boolean().optional(),
1052
+ })
1053
+ .extend(baseSchema.shape),
1054
+ });
1055
+
994
1056
  export const collections = {
995
1057
  events,
996
1058
  commands,
@@ -1024,4 +1086,7 @@ export const collections = {
1024
1086
 
1025
1087
  // Diagrams Collection
1026
1088
  diagrams,
1089
+
1090
+ // Generated from message schema references
1091
+ schemas,
1027
1092
  };
@@ -5,62 +5,55 @@
5
5
 
6
6
  import type { APIRoute } from 'astro';
7
7
  import { getCollection } from 'astro:content';
8
- import path from 'node:path';
9
- import fs from 'node:fs';
10
- import utils from '@eventcatalog/sdk';
11
8
  import { isEventCatalogScaleEnabled } from '@utils/feature';
12
9
  import { sortVersioned } from '@utils/collections/util';
13
10
 
14
- export async function getStaticPaths() {
15
- const events = await getCollection('events');
16
- const commands = await getCollection('commands');
17
- const queries = await getCollection('queries');
18
- const messages = [...events, ...commands, ...queries];
11
+ const findSchema = async (collection: string | undefined, id: string, version: string | undefined) => {
12
+ const schemas = await getCollection('schemas');
13
+ const matchingSchemas = schemas.filter(
14
+ (schema) => schema.data.message.collection === collection && schema.data.message.id === id
15
+ );
16
+
17
+ if (version === 'latest') {
18
+ return (
19
+ matchingSchemas.find((schema) => schema.data.latest) ??
20
+ sortVersioned(matchingSchemas, (schema) => schema.data.message.version)[0]
21
+ );
22
+ }
23
+
24
+ return matchingSchemas.find((schema) => schema.data.message.version === version);
25
+ };
19
26
 
20
- const messagesWithSchemas = messages
21
- .filter((message) => message.data.schemaPath)
22
- .filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? '')));
27
+ export async function getStaticPaths() {
28
+ const schemas = await getCollection('schemas');
23
29
 
24
30
  // Generate paths for specific versions
25
- const versionedPaths = messagesWithSchemas.map((message) => ({
26
- params: { collection: message.collection, id: message.data.id, version: message.data.version },
31
+ const versionedPaths = schemas.map((schema) => ({
32
+ params: {
33
+ collection: schema.data.message.collection,
34
+ id: schema.data.message.id,
35
+ version: schema.data.message.version,
36
+ },
27
37
  props: {
28
- pathToSchema: path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''),
29
- schema: fs.readFileSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''), 'utf8'),
30
- extension: message.data.schemaPath?.split('.').pop(),
38
+ pathToSchema: schema.data.filePath,
39
+ schema: schema.data.content,
31
40
  },
32
41
  }));
33
42
 
34
- // Group messages by collection and id to find latest versions
35
- const groupedMessages = messagesWithSchemas.reduce(
36
- (acc, message) => {
37
- const key = `${message.collection}:${message.data.id}`;
38
- if (!acc[key]) {
39
- acc[key] = [];
40
- }
41
- acc[key].push(message);
42
- return acc;
43
- },
44
- {} as Record<string, typeof messagesWithSchemas>
45
- );
46
-
47
43
  // Generate "latest" paths for each unique collection/id combination
48
- const latestPaths = Object.values(groupedMessages).map((group) => {
49
- // Sort by version (descending) and get the latest
50
- const sorted = sortVersioned(group, (m) => m.data.version);
51
- const latestMessage = sorted[0];
52
- return {
53
- params: { collection: latestMessage.collection, id: latestMessage.data.id, version: 'latest' },
44
+ const latestPaths = schemas
45
+ .filter((schema) => schema.data.latest)
46
+ .map((latestSchema) => ({
47
+ params: {
48
+ collection: latestSchema.data.message.collection,
49
+ id: latestSchema.data.message.id,
50
+ version: 'latest',
51
+ },
54
52
  props: {
55
- pathToSchema: path.join(path.dirname(latestMessage.filePath ?? ''), latestMessage.data.schemaPath ?? ''),
56
- schema: fs.readFileSync(
57
- path.join(path.dirname(latestMessage.filePath ?? ''), latestMessage.data.schemaPath ?? ''),
58
- 'utf8'
59
- ),
60
- extension: latestMessage.data.schemaPath?.split('.').pop(),
53
+ pathToSchema: latestSchema.data.filePath,
54
+ schema: latestSchema.data.content,
61
55
  },
62
- };
63
- });
56
+ }));
64
57
 
65
58
  return [...versionedPaths, ...latestPaths];
66
59
  }
@@ -83,14 +76,14 @@ export const GET: APIRoute = async ({ props, params }) => {
83
76
  }
84
77
 
85
78
  // In static mode, props are pre-computed by getStaticPaths
86
- if (props.schema) {
79
+ if (props.schema !== undefined) {
87
80
  return new Response(props.schema, {
88
81
  headers: { 'Content-Type': 'text/plain' },
89
82
  });
90
83
  }
91
84
 
92
- // In SSR mode, dynamically resolve the schema using the SDK
93
- const { id, version } = params;
85
+ // In SSR mode, dynamically resolve the schema using the generated schema collection
86
+ const { collection, id, version } = params;
94
87
 
95
88
  if (!id) {
96
89
  return new Response(JSON.stringify({ error: 'Missing id parameter' }), {
@@ -99,17 +92,16 @@ export const GET: APIRoute = async ({ props, params }) => {
99
92
  });
100
93
  }
101
94
 
102
- const { getSchemaForMessage } = utils(process.env.PROJECT_DIR || '');
103
- const result = await getSchemaForMessage(id, version === 'latest' ? undefined : version);
95
+ const schema = await findSchema(collection, id, version);
104
96
 
105
- if (!result) {
97
+ if (schema?.data.content === undefined) {
106
98
  return new Response(JSON.stringify({ error: 'Schema not found' }), {
107
99
  status: 404,
108
100
  headers: { 'Content-Type': 'application/json' },
109
101
  });
110
102
  }
111
103
 
112
- return new Response(result.schema, {
104
+ return new Response(schema.data.content, {
113
105
  headers: { 'Content-Type': 'text/plain' },
114
106
  });
115
107
  };
@@ -28,6 +28,8 @@ import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-gr
28
28
  import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
29
29
  import config from '@config';
30
30
 
31
+ const MESSAGE_COLLECTIONS = new Set(['events', 'commands', 'queries']);
32
+
31
33
  // ============================================
32
34
  // Pagination utilities
33
35
  // ============================================
@@ -226,6 +228,27 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
226
228
  };
227
229
  }
228
230
 
231
+ if (MESSAGE_COLLECTIONS.has(params.resourceCollection)) {
232
+ const schemaEntries = await getCollection('schemas');
233
+ const schemas = schemaEntries.filter(
234
+ (schema) =>
235
+ schema.data.message.collection === params.resourceCollection &&
236
+ schema.data.message.id === params.resourceId &&
237
+ schema.data.message.version === params.resourceVersion
238
+ );
239
+
240
+ if (schemas.length > 0) {
241
+ return schemas.map((schema) => ({
242
+ id: schema.id,
243
+ name: schema.data.name,
244
+ ref: schema.data.ref,
245
+ format: schema.data.format,
246
+ source: schema.data.source,
247
+ code: schema.data.content || '',
248
+ }));
249
+ }
250
+ }
251
+
229
252
  const schema = await getSchemasFromResource(resource);
230
253
 
231
254
  if (schema.length > 0) {
@@ -565,7 +565,7 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
565
565
  <div data-pagefind-ignore>
566
566
  <!-- @ts-ignore -->
567
567
  <!-- <SchemaViewer id={props.data.id} catalog={props.catalog} filePath={props.filePath} /> -->
568
- <SchemaViewer id={props.data.id} filePath={props.filePath} />
568
+ <SchemaViewer id={props.data.id} version={props.data.version} collection={props.collection} filePath={props.filePath} />
569
569
 
570
570
  {
571
571
  nodeGraphs.length > 0 &&
@@ -1,14 +1,13 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import config from '@config';
3
3
  import type { APIRoute } from 'astro';
4
- import type { CollectionEntry } from 'astro:content';
5
4
  import { getSpecificationsForService } from '@utils/collections/services';
6
5
  import { isEventCatalogScaleEnabled } from '@utils/feature';
7
6
 
8
- const events = await getCollection('events');
9
- const commands = await getCollection('commands');
10
- const queries = await getCollection('queries');
7
+ type MessageCollection = 'events' | 'commands' | 'queries';
8
+
11
9
  const services = await getCollection('services');
10
+ const schemas = await getCollection('schemas');
12
11
 
13
12
  type ServiceWithSchema = {
14
13
  collection: string;
@@ -33,8 +32,19 @@ const servicesWithSchemasFlat = servicesWithSchemas.reduce<ServiceWithSchema[]>(
33
32
  ];
34
33
  }, []) as ServiceWithSchema[];
35
34
 
36
- const messageHasSchema = (message: CollectionEntry<'events' | 'commands' | 'queries'>) => {
37
- return message.data.schemaPath;
35
+ const getMessagesWithSchemas = (collection: MessageCollection) => {
36
+ const seenMessages = new Set<string>();
37
+
38
+ return schemas
39
+ .filter((schema) => schema.data.message.collection === collection)
40
+ .map((schema) => {
41
+ const key = `${schema.data.message.collection}:${schema.data.message.id}:${schema.data.message.version}`;
42
+ if (seenMessages.has(key)) return null;
43
+ seenMessages.add(key);
44
+
45
+ return schema;
46
+ })
47
+ .filter((schema): schema is NonNullable<typeof schema> => schema !== null);
38
48
  };
39
49
 
40
50
  export const GET: APIRoute = async ({ params, request }) => {
@@ -51,8 +61,9 @@ export const GET: APIRoute = async ({ params, request }) => {
51
61
  const url = new URL(request.url);
52
62
  const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`;
53
63
 
54
- const formatVersionedItem = (item: any, type: string, extraParams?: string | string[]) => {
55
- return `- [${item.data.name} - ${item.data.id} - ${item.data.version}](${baseUrl}/api/schemas/${type}/${item.data.id}/${item.data.version})} ${item.data.summary ? `- ${item.data.summary.trim()}` : ''}`;
64
+ const formatVersionedItem = (item: (typeof schemas)[number]) => {
65
+ const message = item.data.message;
66
+ return `- [${message.name || message.id} - ${message.id} - ${message.version}](${baseUrl}/api/schemas/${message.collection}/${message.id}/${message.version})} ${message.summary ? `- ${message.summary.trim()}` : ''}`;
56
67
  };
57
68
 
58
69
  const formatServiceWithSchema = (item: ServiceWithSchema) => {
@@ -63,19 +74,16 @@ export const GET: APIRoute = async ({ params, request }) => {
63
74
  `# ${config.organizationName} EventCatalog Schemas`,
64
75
  `List of schemas for events, commands, queries, and services in EventCatalog.`,
65
76
  '',
66
- `## Events\n${events
67
- .filter(messageHasSchema)
68
- .map((item) => formatVersionedItem(item, 'events'))
77
+ `## Events\n${getMessagesWithSchemas('events')
78
+ .map((item) => formatVersionedItem(item))
69
79
  .join('\n')}`,
70
80
  '',
71
- `## Commands\n${commands
72
- .filter(messageHasSchema)
73
- .map((item) => formatVersionedItem(item, 'commands'))
81
+ `## Commands\n${getMessagesWithSchemas('commands')
82
+ .map((item) => formatVersionedItem(item))
74
83
  .join('\n')}`,
75
84
  '',
76
- `## Queries\n${queries
77
- .filter(messageHasSchema)
78
- .map((item) => formatVersionedItem(item, 'queries'))
85
+ `## Queries\n${getMessagesWithSchemas('queries')
86
+ .map((item) => formatVersionedItem(item))
79
87
  .join('\n')}`,
80
88
  '',
81
89
  `## Services\n${servicesWithSchemasFlat.map((item: any) => formatServiceWithSchema(item)).join('\n')}`,
@@ -1,3 +1,4 @@
1
+ import { getCollection } from 'astro:content';
1
2
  import { isSSR } from '@utils/feature';
2
3
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
4
  import type { PageTypes } from '@types';
@@ -23,25 +24,41 @@ export class Page extends HybridPage {
23
24
  // 'entities',
24
25
  // 'containers',
25
26
  ];
26
- const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]()));
27
+ const [schemas, ...allItems] = await Promise.all([
28
+ getCollection('schemas'),
29
+ ...itemTypes.map((type) => pageDataLoader[type]()),
30
+ ]);
31
+ const itemsByKey = new Map(allItems.flat().map((item) => [`${item.collection}:${item.data.id}:${item.data.version}`, item]));
27
32
 
28
- // We only care about any item that has data.schemaPath
29
- const itemsWithSchema = allItems.flatMap((items) => items.filter((item) => item.data.schemaPath));
33
+ const seenMessageSchemas = new Set<string>();
34
+ const messageSchemas = schemas.filter((schema) => {
35
+ const key = `${schema.data.message.collection}:${schema.data.message.id}:${schema.data.message.version}`;
36
+ if (seenMessageSchemas.has(key)) return false;
37
+ seenMessageSchemas.add(key);
38
+ return true;
39
+ });
30
40
 
31
41
  // Generate paths for messages with schemas
32
- const messagePaths = itemsWithSchema.map((item) => ({
33
- params: {
34
- type: item.collection,
35
- id: item.data.id,
36
- version: item.data.version,
37
- },
38
- props: {
39
- type: item.collection,
40
- ...item,
41
- // Not everything needs the body of the page itself.
42
- body: undefined,
43
- },
44
- }));
42
+ const messagePaths = messageSchemas
43
+ .map((schema) => {
44
+ const item = itemsByKey.get(`${schema.data.message.collection}:${schema.data.message.id}:${schema.data.message.version}`);
45
+ if (!item) return null;
46
+
47
+ return {
48
+ params: {
49
+ type: schema.data.message.collection,
50
+ id: schema.data.message.id,
51
+ version: schema.data.message.version,
52
+ },
53
+ props: {
54
+ type: schema.data.message.collection,
55
+ ...item,
56
+ // Not everything needs the body of the page itself.
57
+ body: undefined,
58
+ },
59
+ };
60
+ })
61
+ .filter((path): path is NonNullable<typeof path> => path !== null);
45
62
 
46
63
  // Generate paths for data products with contracts
47
64
  const dataProducts = await pageDataLoader['data-products']();
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { PageTypes } from '@types';
3
+ import { getCollection } from 'astro:content';
3
4
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
4
5
  import { Page } from './_index.data';
5
6
  import SchemaPageViewer from '@components/SchemaExplorer/SchemaPageViewer';
@@ -76,32 +77,41 @@ if (isDataProduct && contractPath) {
76
77
  }
77
78
  } else {
78
79
  // Handle regular messages (events, commands, queries)
79
- const allItems = await pageDataLoader[type]();
80
+ const [allItems, schemaEntries] = await Promise.all([pageDataLoader[type](), getCollection('schemas')]);
80
81
  const versions = allItems.filter((item) => item.data.id === data.id);
82
+ const schemasForMessage = schemaEntries.filter(
83
+ (schema) => schema.data.message.collection === type && schema.data.message.id === data.id
84
+ );
85
+
86
+ const getSchemaForVersion = (version: string) => {
87
+ const schemasForVersion = schemasForMessage.filter((schema) => schema.data.message.version === version);
88
+ return schemasForVersion.find((schema) => schema.data.default) ?? schemasForVersion[0];
89
+ };
81
90
 
82
91
  // Transform to SchemaItems
83
92
  const availableVersions = versions
84
- .filter((message) => message.data.schemaPath && resourceFileExists(message, message.data.schemaPath))
85
93
  .map((message) => {
86
94
  try {
87
- const schemaPath = message.data.schemaPath ?? '';
88
- const schemaContent = readResourceFile(message, schemaPath) ?? '';
95
+ const schema = getSchemaForVersion(message.data.version);
96
+ if (!schema) return null;
97
+
98
+ const schemaPath = schema.data.file || schema.data.source.path || '';
89
99
  const schemaExtension = path.extname(schemaPath).slice(1);
90
100
 
91
101
  return {
92
102
  collection: message.collection,
93
103
  data: {
94
104
  id: message.data.id,
95
- name: message.data.name,
105
+ name: schema.data.message.name || message.data.name,
96
106
  version: message.data.version,
97
- summary: message.data.summary,
98
- schemaPath: message.data.schemaPath,
107
+ summary: schema.data.message.summary || message.data.summary,
108
+ schemaPath,
99
109
  // @ts-ignore
100
110
  producers: message.data.producers || [],
101
111
  // @ts-ignore
102
112
  consumers: message.data.consumers || [],
103
113
  },
104
- schemaContent,
114
+ schemaContent: schema.data.content || '',
105
115
  schemaExtension,
106
116
  examples: getExamplesForResource(message),
107
117
  } as SchemaItem;
@@ -10,6 +10,7 @@ import { getOwner } from '@utils/collections/owners';
10
10
  import { buildUrl } from '@utils/url-builder';
11
11
  import { resourceFileExists, readResourceFile } from '@utils/resource-files';
12
12
  import { getExamplesForResource } from '@utils/collections/examples';
13
+ import { getCollection } from 'astro:content';
13
14
  import path from 'path';
14
15
 
15
16
  // Helper function to enrich owners with full details
@@ -32,61 +33,84 @@ async function fetchAllSchemas() {
32
33
  const events = await getEvents({ getAllVersions: true });
33
34
  const commands = await getCommands({ getAllVersions: true });
34
35
  const queries = await getQueries({ getAllVersions: true });
36
+ const schemaEntries = await getCollection('schemas');
35
37
 
36
38
  // Fetch all services
37
39
  const services = await getServices({ getAllVersions: true });
38
40
 
39
41
  // Combine all messages
40
42
  const allMessages = [...events, ...commands, ...queries];
43
+ const messagesBySchemaReference = new Map(
44
+ allMessages.map((message) => [`${message.collection}:${message.data.id}:${message.data.version}`, message])
45
+ );
41
46
 
42
- // Filter messages with schemas and read schema content - only keep essential data
47
+ // Read message schemas from the generated schemas collection.
43
48
  const messagesWithSchemas = await Promise.all(
44
- allMessages
45
- .filter((message) => message.data.schemaPath)
46
- .filter((message) => resourceFileExists(message, message.data.schemaPath ?? ''))
47
- .map(async (message) => {
49
+ schemaEntries.map(async (schema) => {
50
+ const message = messagesBySchemaReference.get(
51
+ `${schema.data.message.collection}:${schema.data.message.id}:${schema.data.message.version}`
52
+ );
53
+ const schemaPath = schema.data.file || schema.data.source.path || '';
54
+ const schemaExtension = path.extname(schemaPath).slice(1) || schema.data.format;
55
+
56
+ if (message) {
48
57
  try {
49
- const schemaPath = message.data.schemaPath ?? '';
50
- const schemaContent = readResourceFile(message, schemaPath) ?? '';
51
- const schemaExtension = path.extname(schemaPath).slice(1);
52
- const enrichedOwners = await enrichOwners(message.data.owners || []);
58
+ const enrichedOwners = await enrichOwners(schema.data.message.owners || []);
53
59
 
54
60
  return {
55
61
  collection: message.collection,
56
62
  data: {
57
63
  id: message.data.id,
58
- name: message.data.name,
64
+ name: schema.data.message.name || message.data.name,
59
65
  version: message.data.version,
60
- summary: message.data.summary,
61
- schemaPath: message.data.schemaPath,
66
+ summary: schema.data.message.summary || message.data.summary,
67
+ schemaPath,
62
68
  producers: message.data.producers || [],
63
69
  consumers: message.data.consumers || [],
64
70
  owners: enrichedOwners,
65
71
  },
66
- schemaContent,
72
+ schemaContent: schema.data.content || '',
67
73
  schemaExtension,
68
74
  examples: getExamplesForResource(message),
69
75
  };
70
76
  } catch (error) {
71
- console.error(`Error reading schema for ${message.data.id}:`, error);
72
- const enrichedOwners = await enrichOwners(message.data.owners || []);
77
+ console.error(`Error reading schema metadata for ${message.data.id}:`, error);
78
+ const enrichedOwners = await enrichOwners(schema.data.message.owners || []);
73
79
  return {
74
80
  collection: message.collection,
75
81
  data: {
76
82
  id: message.data.id,
77
- name: message.data.name,
83
+ name: schema.data.message.name || schema.data.name || message.data.name,
78
84
  version: message.data.version,
79
- summary: message.data.summary,
80
- schemaPath: message.data.schemaPath,
85
+ summary: schema.data.message.summary || message.data.summary,
86
+ schemaPath,
81
87
  producers: message.data.producers || [],
82
88
  consumers: message.data.consumers || [],
83
89
  owners: enrichedOwners,
84
90
  },
85
- schemaContent: '',
86
- schemaExtension: 'json',
91
+ schemaContent: schema.data.content || '',
92
+ schemaExtension,
87
93
  };
88
94
  }
89
- })
95
+ }
96
+
97
+ return {
98
+ collection: schema.data.message.collection,
99
+ data: {
100
+ id: schema.data.message.id,
101
+ name: schema.data.message.name || schema.data.name || schema.data.message.id,
102
+ version: schema.data.message.version,
103
+ summary: schema.data.message.summary,
104
+ schemaPath,
105
+ owners: await enrichOwners(schema.data.message.owners || []),
106
+ producers: [],
107
+ consumers: [],
108
+ },
109
+ schemaContent: schema.data.content || '',
110
+ schemaExtension,
111
+ examples: [],
112
+ };
113
+ })
90
114
  );
91
115
 
92
116
  // Filter services with specifications and read spec content - only keep essential data
@@ -15,11 +15,33 @@ import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
15
15
  import { iconFieldsForResource } from '@utils/icon';
16
16
  import { collectionToResourceMap } from '@utils/collections/util';
17
17
 
18
+ type MessageSchemaEntry = CollectionEntry<'schemas'>;
19
+
18
20
  const getProducerConsumerPageRef = (resource: any) => {
19
21
  const resourceType = collectionToResourceMap[resource.collection as keyof typeof collectionToResourceMap];
20
22
  return `${resourceType}:${resource.data.id}:${resource.data.version}`;
21
23
  };
22
24
 
25
+ const getSchemasForMessage = (
26
+ message: CollectionEntry<'events' | 'commands' | 'queries'>,
27
+ schemas: MessageSchemaEntry[] = []
28
+ ) => {
29
+ return schemas.filter(
30
+ (schema) =>
31
+ schema.data.message.collection === message.collection &&
32
+ schema.data.message.id === message.data.id &&
33
+ schema.data.message.version === message.data.version
34
+ );
35
+ };
36
+
37
+ const getSchemaNavTitle = (schemas: MessageSchemaEntry[]) => {
38
+ if (schemas.length > 1) return 'Schemas';
39
+
40
+ const schemaPath = schemas[0]?.data.file || schemas[0]?.data.source.path || schemas[0]?.data.ref || schemas[0]?.id;
41
+ const format = schemaPath ? getSchemaFormatFromURL(schemaPath) : schemas[0]?.data.format;
42
+ return format ? `Schema (${format.toUpperCase()})` : 'Schema';
43
+ };
44
+
23
45
  export const buildMessageNode = (
24
46
  message: CollectionEntry<'events' | 'commands' | 'queries'>,
25
47
  owners: any[],
@@ -51,7 +73,8 @@ export const buildMessageNode = (
51
73
  };
52
74
  const defaultIcon = iconMap[collection] || 'Mail';
53
75
 
54
- const hasSchema = message.data.schemaPath !== undefined;
76
+ const resolvedSchemas = getSchemasForMessage(message, context.schemas);
77
+ const hasSchema = resolvedSchemas.length > 0;
55
78
  const renderVisualiser = isVisualiserEnabled();
56
79
  const docsSection = buildResourceDocsSection(
57
80
  collection as 'events' | 'commands' | 'queries',
@@ -116,7 +139,7 @@ export const buildMessageNode = (
116
139
  pages: [
117
140
  {
118
141
  type: 'item',
119
- title: `Schema (${getSchemaFormatFromURL(message.data.schemaPath!).toUpperCase()})`,
142
+ title: getSchemaNavTitle(resolvedSchemas),
120
143
  href: buildUrl(`/schemas/${collection}/${message.data.id}/${message.data.version}`),
121
144
  },
122
145
  hasFieldUsage && {