@eventcatalog/core 3.44.1 → 3.45.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.
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(analytics_exports);
36
36
  var import_os = __toESM(require("os"), 1);
37
37
 
38
38
  // package.json
39
- var version = "3.44.1";
39
+ var version = "3.45.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-MFLSPCEA.js";
4
- import "../chunk-EPDZMVQ3.js";
3
+ } from "../chunk-6XNMBNIU.js";
4
+ import "../chunk-E4K4KTSR.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
140
140
  var import_os = __toESM(require("os"), 1);
141
141
 
142
142
  // package.json
143
- var version = "3.44.1";
143
+ var version = "3.45.0";
144
144
 
145
145
  // src/constants.ts
146
146
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-DKKPJ3QM.js";
4
- import "../chunk-MFLSPCEA.js";
3
+ } from "../chunk-2QK37BNF.js";
4
+ import "../chunk-6XNMBNIU.js";
5
5
  import "../chunk-DAOXTQVS.js";
6
- import "../chunk-EPDZMVQ3.js";
6
+ import "../chunk-E4K4KTSR.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-MFLSPCEA.js";
3
+ } from "./chunk-6XNMBNIU.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-EPDZMVQ3.js";
3
+ } from "./chunk-E4K4KTSR.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "3.44.1";
2
+ var version = "3.45.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-EPDZMVQ3.js";
3
+ } from "./chunk-E4K4KTSR.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-L26L4Y5J.js";
3
+ } from "./chunk-G6KYCMUM.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "3.44.1";
28
+ var version = "3.45.0";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-EPDZMVQ3.js";
3
+ } from "./chunk-E4K4KTSR.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "3.44.1";
147
+ var version = "3.45.0";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -13,8 +13,8 @@ import {
13
13
  } from "./chunk-3H2RT3CM.js";
14
14
  import {
15
15
  log_build_default
16
- } from "./chunk-DKKPJ3QM.js";
17
- import "./chunk-MFLSPCEA.js";
16
+ } from "./chunk-2QK37BNF.js";
17
+ import "./chunk-6XNMBNIU.js";
18
18
  import "./chunk-DAOXTQVS.js";
19
19
  import {
20
20
  catalogToAstro
@@ -28,13 +28,13 @@ import {
28
28
  } from "./chunk-B7HCX5HM.js";
29
29
  import {
30
30
  generate
31
- } from "./chunk-3UVY6YHH.js";
31
+ } from "./chunk-KHL25572.js";
32
32
  import {
33
33
  logger
34
- } from "./chunk-L26L4Y5J.js";
34
+ } from "./chunk-G6KYCMUM.js";
35
35
  import {
36
36
  VERSION
37
- } from "./chunk-EPDZMVQ3.js";
37
+ } from "./chunk-E4K4KTSR.js";
38
38
  import {
39
39
  getEventCatalogConfigFile,
40
40
  verifyRequiredFieldsAreInCatalogConfigFile
package/dist/generate.cjs CHANGED
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
108
108
  var import_picocolors = __toESM(require("picocolors"), 1);
109
109
 
110
110
  // package.json
111
- var version = "3.44.1";
111
+ var version = "3.45.0";
112
112
 
113
113
  // src/constants.ts
114
114
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-3UVY6YHH.js";
4
- import "./chunk-L26L4Y5J.js";
5
- import "./chunk-EPDZMVQ3.js";
3
+ } from "./chunk-KHL25572.js";
4
+ import "./chunk-G6KYCMUM.js";
5
+ import "./chunk-E4K4KTSR.js";
6
6
  import "./chunk-6QENHZZP.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "3.44.1";
39
+ var version = "3.45.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-L26L4Y5J.js";
4
- import "../chunk-EPDZMVQ3.js";
3
+ } from "../chunk-G6KYCMUM.js";
4
+ import "../chunk-E4K4KTSR.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildEnvironmentUrl, findCurrentEnvironment } from './EnvironmentDropdown';
3
+
4
+ const environments = [
5
+ {
6
+ name: 'Development',
7
+ url: 'https://example.com/event-catalog/dev',
8
+ shortName: 'Dev',
9
+ },
10
+ {
11
+ name: 'UAT',
12
+ url: 'https://example.com/event-catalog/uat',
13
+ shortName: 'UAT',
14
+ },
15
+ {
16
+ name: 'Production',
17
+ url: 'https://example.com/event-catalog/prod',
18
+ shortName: 'Prod',
19
+ },
20
+ ];
21
+
22
+ describe('EnvironmentDropdown', () => {
23
+ it('finds the current environment when environments share an origin but use different base paths', () => {
24
+ expect(findCurrentEnvironment(environments, 'https://example.com/event-catalog/uat/services/foo')?.name).toBe('UAT');
25
+ });
26
+
27
+ it('builds environment urls by replacing the current environment base path', () => {
28
+ expect(
29
+ buildEnvironmentUrl(
30
+ 'https://example.com/event-catalog/prod',
31
+ 'https://example.com/event-catalog/uat/services/foo',
32
+ 'https://example.com/event-catalog/uat'
33
+ )
34
+ ).toBe('https://example.com/event-catalog/prod/services/foo');
35
+ });
36
+
37
+ it('preserves search params and hashes when switching environments', () => {
38
+ expect(
39
+ buildEnvironmentUrl(
40
+ 'https://example.com/event-catalog/prod',
41
+ 'https://example.com/event-catalog/uat/services/foo?tab=messages#latest',
42
+ 'https://example.com/event-catalog/uat'
43
+ )
44
+ ).toBe('https://example.com/event-catalog/prod/services/foo?tab=messages#latest');
45
+ });
46
+
47
+ it('keeps existing origin based environment switching for root catalogs', () => {
48
+ expect(
49
+ buildEnvironmentUrl('https://prod.example.com', 'https://uat.example.com/services/foo', 'https://uat.example.com')
50
+ ).toBe('https://prod.example.com/services/foo');
51
+ });
52
+
53
+ it('switches from a root catalog to a subpath catalog', () => {
54
+ expect(buildEnvironmentUrl('https://example.com/prod', 'https://example.com/services/foo', 'https://example.com')).toBe(
55
+ 'https://example.com/prod/services/foo'
56
+ );
57
+ });
58
+
59
+ it('preserves the current path when the current environment is unknown', () => {
60
+ expect(buildEnvironmentUrl('https://example.com/prod', 'http://localhost:3000/services/foo')).toBe(
61
+ 'https://example.com/prod/services/foo'
62
+ );
63
+ });
64
+ });
@@ -11,20 +11,56 @@ interface EnvironmentDropdownProps {
11
11
  environments: Environment[];
12
12
  }
13
13
 
14
+ const stripTrailingSlash = (pathname: string) => pathname.replace(/\/$/, '') || '/';
15
+
16
+ const startsWithPath = (pathname: string, basePathname: string) =>
17
+ basePathname === '/' || pathname === basePathname || pathname.startsWith(`${basePathname}/`);
18
+
19
+ export const findCurrentEnvironment = (environments: Environment[], currentHref: string) => {
20
+ const currentUrl = new URL(currentHref);
21
+
22
+ return (
23
+ environments
24
+ .filter((env) => {
25
+ const envUrl = new URL(env.url, currentUrl.href);
26
+ const envPathname = stripTrailingSlash(envUrl.pathname);
27
+
28
+ return envUrl.origin === currentUrl.origin && startsWithPath(currentUrl.pathname, envPathname);
29
+ })
30
+ .sort(
31
+ (a, b) =>
32
+ stripTrailingSlash(new URL(b.url, currentUrl.href).pathname).length -
33
+ stripTrailingSlash(new URL(a.url, currentUrl.href).pathname).length
34
+ )[0] || null
35
+ );
36
+ };
37
+
38
+ export const buildEnvironmentUrl = (environmentUrl: string, currentHref: string, currentEnvironmentUrl?: string) => {
39
+ const currentUrl = new URL(currentHref);
40
+ const targetUrl = new URL(environmentUrl, currentUrl.href);
41
+ const targetBasePathname = stripTrailingSlash(targetUrl.pathname);
42
+ const currentBasePathname = currentEnvironmentUrl
43
+ ? stripTrailingSlash(new URL(currentEnvironmentUrl, currentUrl.href).pathname)
44
+ : undefined;
45
+ const pathWithinEnvironment =
46
+ currentBasePathname && currentBasePathname !== '/' && startsWithPath(currentUrl.pathname, currentBasePathname)
47
+ ? currentUrl.pathname.slice(currentBasePathname.length)
48
+ : currentUrl.pathname;
49
+
50
+ targetUrl.pathname = `${targetBasePathname}${pathWithinEnvironment}`.replace(/\/+/g, '/');
51
+ targetUrl.search = currentUrl.search;
52
+ targetUrl.hash = currentUrl.hash;
53
+
54
+ return targetUrl.toString();
55
+ };
56
+
14
57
  export const EnvironmentDropdown: React.FC<EnvironmentDropdownProps> = ({ environments }) => {
15
58
  const [isOpen, setIsOpen] = useState(false);
16
59
  const [currentEnvironment, setCurrentEnvironment] = useState<Environment | null>(null);
17
60
  const dropdownRef = useRef<HTMLDivElement>(null);
18
61
 
19
62
  useEffect(() => {
20
- // Check if current URL matches any environment
21
- const currentUrl = window.location.origin;
22
- const matchedEnv = environments.find((env) => {
23
- // Normalize URLs for comparison
24
- const envUrl = new URL(env.url).origin;
25
- return envUrl === currentUrl;
26
- });
27
- setCurrentEnvironment(matchedEnv || null);
63
+ setCurrentEnvironment(findCurrentEnvironment(environments, window.location.href));
28
64
  }, [environments]);
29
65
 
30
66
  useEffect(() => {
@@ -99,11 +135,7 @@ export const EnvironmentDropdown: React.FC<EnvironmentDropdownProps> = ({ enviro
99
135
  href={env.url}
100
136
  onClick={(e) => {
101
137
  e.preventDefault();
102
- // Construct the full URL with the current path when clicked
103
- const currentPath = window.location.pathname + window.location.search + window.location.hash;
104
- const targetUrl = new URL(env.url);
105
- targetUrl.pathname = currentPath;
106
- window.location.href = targetUrl.toString();
138
+ window.location.href = buildEnvironmentUrl(env.url, window.location.href, currentEnvironment?.url);
107
139
  }}
108
140
  className={`block rounded-xl px-3 py-3 transition-colors ${
109
141
  isCurrentEnv
@@ -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,16 @@ const channelPointer = z
68
69
  })
69
70
  .extend(pointer.shape);
70
71
 
72
+ const schemaPointer = z.object({
73
+ id: z.string().optional(),
74
+ file: z.string().optional(),
75
+ path: z.string().optional(),
76
+ name: z.string().optional(),
77
+ format: z.string().optional(),
78
+ environments: z.array(z.string()).optional(),
79
+ default: z.boolean().optional(),
80
+ });
81
+
71
82
  const sendsPointer = z.object({
72
83
  id: z.string(),
73
84
  version: z.string().optional().default('latest'),
@@ -375,6 +386,7 @@ const events = defineCollection({
375
386
  producers: z.array(reference('services')).optional(),
376
387
  consumers: z.array(reference('services')).optional(),
377
388
  channels: z.array(channelPointer).optional(),
389
+ schemas: z.array(schemaPointer).optional(),
378
390
  messageChannels: z.array(reference('channels')).optional(),
379
391
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
380
392
  })
@@ -397,6 +409,7 @@ const commands = defineCollection({
397
409
  producers: z.array(reference('services')).optional(),
398
410
  consumers: z.array(reference('services')).optional(),
399
411
  channels: z.array(channelPointer).optional(),
412
+ schemas: z.array(schemaPointer).optional(),
400
413
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
401
414
  messageChannels: z.array(reference('channels')).optional(),
402
415
  })
@@ -419,6 +432,7 @@ const queries = defineCollection({
419
432
  producers: z.array(reference('services')).optional(),
420
433
  consumers: z.array(reference('services')).optional(),
421
434
  channels: z.array(channelPointer).optional(),
435
+ schemas: z.array(schemaPointer).optional(),
422
436
  detailsPanel: messageDetailsPanelPropertySchema.optional(),
423
437
  messageChannels: z.array(reference('channels')).optional(),
424
438
  })
@@ -991,6 +1005,47 @@ const diagrams = defineCollection({
991
1005
  .extend(baseSchema.shape),
992
1006
  });
993
1007
 
1008
+ const schemas = defineCollection({
1009
+ loader: schemaLoader({
1010
+ messages: {
1011
+ pattern: withIgnoredBuildArtifacts([
1012
+ '**/events/*/index.{md,mdx}',
1013
+ '**/events/*/versioned/*/index.{md,mdx}',
1014
+ '**/commands/*/index.{md,mdx}',
1015
+ '**/commands/*/versioned/*/index.{md,mdx}',
1016
+ '**/queries/*/index.{md,mdx}',
1017
+ '**/queries/*/versioned/*/index.{md,mdx}',
1018
+ ]) as string[],
1019
+ base: projectDirBase,
1020
+ },
1021
+ }),
1022
+ schema: z
1023
+ .object({
1024
+ format: z.string(),
1025
+ content: z.string().optional(),
1026
+ file: z.string().optional(),
1027
+ filePath: z.string().optional(),
1028
+ environments: z.array(z.string()).optional(),
1029
+ default: z.boolean().optional(),
1030
+ latest: z.boolean().optional(),
1031
+ message: z.object({
1032
+ collection: z.enum(['events', 'commands', 'queries']),
1033
+ id: z.string(),
1034
+ name: z.string().optional(),
1035
+ version: z.string(),
1036
+ summary: z.string().optional(),
1037
+ owners: z.array(z.string()).optional(),
1038
+ }),
1039
+ source: z.object({
1040
+ provider: z.string(),
1041
+ path: z.string().optional(),
1042
+ url: z.string().optional(),
1043
+ }),
1044
+ readOnly: z.boolean().optional(),
1045
+ })
1046
+ .extend(baseSchema.shape),
1047
+ });
1048
+
994
1049
  export const collections = {
995
1050
  events,
996
1051
  commands,
@@ -1024,4 +1079,7 @@ export const collections = {
1024
1079
 
1025
1080
  // Diagrams Collection
1026
1081
  diagrams,
1082
+
1083
+ // Generated from message schema references
1084
+ schemas,
1027
1085
  };
@@ -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
  };
@@ -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
@@ -0,0 +1,237 @@
1
+ import type { Loader } from 'astro/loaders';
2
+ import { glob } from 'glob';
3
+ import matter from 'gray-matter';
4
+ import fs from 'node:fs/promises';
5
+ import fsSync from 'node:fs';
6
+ import path from 'node:path';
7
+ import { sortVersioned } from './util';
8
+
9
+ type MessageCollection = 'events' | 'commands' | 'queries';
10
+
11
+ type SchemaReference = {
12
+ id?: string;
13
+ file?: string;
14
+ path?: string;
15
+ name?: string;
16
+ format?: string;
17
+ environments?: string[];
18
+ default?: boolean;
19
+ };
20
+
21
+ type MessageFrontmatter = {
22
+ id?: string;
23
+ name?: string;
24
+ version?: string;
25
+ summary?: string;
26
+ owners?: string[];
27
+ schemaPath?: string;
28
+ schemas?: SchemaReference[];
29
+ };
30
+
31
+ export type MessageSchemaResource = {
32
+ id: string;
33
+ name?: string;
34
+ version?: string;
35
+ format: string;
36
+ content?: string;
37
+ file?: string;
38
+ filePath?: string;
39
+ environments?: string[];
40
+ default?: boolean;
41
+ latest?: boolean;
42
+ message: {
43
+ collection: MessageCollection;
44
+ id: string;
45
+ name?: string;
46
+ version: string;
47
+ summary?: string;
48
+ owners?: string[];
49
+ };
50
+ source: {
51
+ provider: 'file';
52
+ path: string;
53
+ };
54
+ };
55
+
56
+ type SchemaLoaderOptions = {
57
+ messages: {
58
+ pattern: string[];
59
+ base?: string;
60
+ };
61
+ };
62
+
63
+ type LoaderContext = Parameters<Loader['load']>[0];
64
+
65
+ const MESSAGE_COLLECTIONS: MessageCollection[] = ['events', 'commands', 'queries'];
66
+
67
+ const normalizePath = (value: string) => value.replace(/\\/g, '/');
68
+
69
+ const getMessageCollectionFromPath = (filePath: string): MessageCollection | undefined => {
70
+ const parts = normalizePath(filePath).split('/');
71
+ return MESSAGE_COLLECTIONS.find((collection) => parts.includes(collection));
72
+ };
73
+
74
+ const getSchemaFormat = (schemaPath: string) => {
75
+ const extension = path.extname(schemaPath).replace('.', '').toLowerCase();
76
+
77
+ if (extension === 'avsc' || extension === 'avro') return 'avro';
78
+ if (extension === 'proto') return 'protobuf';
79
+ if (extension === 'json') return 'jsonschema';
80
+ if (extension === 'yaml' || extension === 'yml') return 'yaml';
81
+
82
+ return extension || 'unknown';
83
+ };
84
+
85
+ const buildGeneratedSchemaId = (message: { collection: MessageCollection; id: string; version: string }, schemaPath: string) =>
86
+ `schema:${message.collection}:${message.id}:${message.version}:${schemaPath}`;
87
+
88
+ const getSchemaFile = (reference: SchemaReference) => reference.file ?? reference.path;
89
+
90
+ const schemaFileExists = (schema: MessageSchemaResource): schema is MessageSchemaResource & { filePath: string } =>
91
+ Boolean(schema.filePath && fsSync.existsSync(schema.filePath));
92
+
93
+ const addLatestMetadata = (schemas: MessageSchemaResource[]) => {
94
+ const schemasByMessage = schemas.reduce(
95
+ (acc, schema) => {
96
+ const key = `${schema.message.collection}:${schema.message.id}`;
97
+ acc[key] = [...(acc[key] ?? []), schema];
98
+ return acc;
99
+ },
100
+ {} as Record<string, MessageSchemaResource[]>
101
+ );
102
+
103
+ const latestVersionsByMessage = Object.entries(schemasByMessage).reduce(
104
+ (acc, [key, messageSchemas]) => {
105
+ acc[key] = sortVersioned(messageSchemas, (schema) => schema.message.version)[0]?.message.version;
106
+ return acc;
107
+ },
108
+ {} as Record<string, string | undefined>
109
+ );
110
+
111
+ return schemas.map((schema) => {
112
+ const key = `${schema.message.collection}:${schema.message.id}`;
113
+ return {
114
+ ...schema,
115
+ latest: schema.message.version === latestVersionsByMessage[key],
116
+ };
117
+ });
118
+ };
119
+
120
+ export const getMessageSchemasFromFrontmatter = ({
121
+ data,
122
+ collection,
123
+ messageFilePath,
124
+ }: {
125
+ data: MessageFrontmatter;
126
+ collection: MessageCollection;
127
+ messageFilePath: string;
128
+ }): MessageSchemaResource[] => {
129
+ if (!data.id || !data.version) return [];
130
+
131
+ const message = {
132
+ collection,
133
+ id: data.id,
134
+ name: data.name,
135
+ version: data.version,
136
+ summary: data.summary,
137
+ owners: data.owners,
138
+ };
139
+
140
+ const schemaReferences =
141
+ data.schemas?.filter((schema) => getSchemaFile(schema)) ??
142
+ (data.schemaPath
143
+ ? [
144
+ {
145
+ id: buildGeneratedSchemaId(message, data.schemaPath),
146
+ file: data.schemaPath,
147
+ name: 'Schema',
148
+ default: true,
149
+ },
150
+ ]
151
+ : []);
152
+
153
+ return schemaReferences.map((reference) => {
154
+ const schemaFile = getSchemaFile(reference) as string;
155
+ const schemaFilePath = path.resolve(path.dirname(messageFilePath), schemaFile);
156
+ const schemaId = reference.id ?? buildGeneratedSchemaId(message, schemaFile);
157
+
158
+ return {
159
+ id: schemaId,
160
+ name: reference.name ?? schemaId,
161
+ version: data.version,
162
+ format: reference.format ?? getSchemaFormat(schemaFile),
163
+ file: schemaFile,
164
+ filePath: schemaFilePath,
165
+ environments: reference.environments,
166
+ default: reference.default,
167
+ message,
168
+ source: {
169
+ provider: 'file',
170
+ path: schemaFile,
171
+ },
172
+ };
173
+ });
174
+ };
175
+
176
+ export const loadMessageSchemas = async ({ pattern, base }: SchemaLoaderOptions['messages']) => {
177
+ if (!base) return [];
178
+
179
+ const files = await glob(pattern, {
180
+ cwd: base,
181
+ absolute: true,
182
+ nodir: true,
183
+ ignore: ['dist/**', '**/dist/**'],
184
+ });
185
+
186
+ const schemas = await Promise.all(
187
+ files.map(async (file) => {
188
+ const collection = getMessageCollectionFromPath(file);
189
+ if (!collection) return [];
190
+
191
+ const { data } = matter.read(file) as { data: MessageFrontmatter };
192
+ return getMessageSchemasFromFrontmatter({ data, collection, messageFilePath: file });
193
+ })
194
+ );
195
+
196
+ return addLatestMetadata(schemas.flat().filter(schemaFileExists));
197
+ };
198
+
199
+ const getSchemaBody = async (schema: MessageSchemaResource) => {
200
+ if (!schemaFileExists(schema)) return undefined;
201
+ return fs.readFile(schema.filePath, 'utf8');
202
+ };
203
+
204
+ const setSchema = async (context: LoaderContext, schema: MessageSchemaResource) => {
205
+ const body = await getSchemaBody(schema);
206
+ if (body === undefined) return;
207
+
208
+ const schemaWithContent = {
209
+ ...schema,
210
+ content: body,
211
+ };
212
+ const parsedData = await context.parseData({
213
+ id: schema.id,
214
+ data: schemaWithContent,
215
+ });
216
+
217
+ context.store.set({
218
+ id: schema.id,
219
+ data: parsedData,
220
+ body,
221
+ digest: context.generateDigest(schemaWithContent),
222
+ });
223
+ };
224
+
225
+ export const schemaLoader = ({ messages }: SchemaLoaderOptions): Loader => {
226
+ return {
227
+ name: 'eventcatalog-schema-loader',
228
+ load: async (context) => {
229
+ context.store.clear();
230
+ const schemas = await loadMessageSchemas(messages);
231
+
232
+ for (const schema of schemas) {
233
+ await setSchema(context, schema);
234
+ }
235
+ },
236
+ };
237
+ };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "3.44.1",
10
+ "version": "3.45.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -113,8 +113,8 @@
113
113
  "uuid": "^10.0.0",
114
114
  "zod": "^4.3.6",
115
115
  "@eventcatalog/sdk": "2.24.1",
116
- "@eventcatalog/visualiser": "^3.22.1",
117
- "@eventcatalog/linter": "1.0.29"
116
+ "@eventcatalog/linter": "1.0.29",
117
+ "@eventcatalog/visualiser": "^3.22.1"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@astrojs/check": "^0.9.9",