@eventcatalog/core 3.45.0 → 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 (34) 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-E4K4KTSR.js → chunk-DOHA5HNJ.js} +1 -1
  6. package/dist/{chunk-2QK37BNF.js → chunk-FNOYJEUK.js} +1 -1
  7. package/dist/{chunk-6XNMBNIU.js → chunk-JS6IYB55.js} +1 -1
  8. package/dist/{chunk-G6KYCMUM.js → chunk-TLLUDBO4.js} +1 -1
  9. package/dist/{chunk-KHL25572.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 +7 -0
  27. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +23 -0
  28. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -1
  29. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +25 -2
  30. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -0
  31. package/eventcatalog/src/stores/sidebar-store/state.ts +3 -0
  32. package/eventcatalog/src/utils/collections/schema-loader.ts +302 -16
  33. package/eventcatalog/src/utils/files.ts +1 -1
  34. package/package.json +3 -3
@@ -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.45.0";
39
+ var version = "3.46.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-6XNMBNIU.js";
4
- import "../chunk-E4K4KTSR.js";
3
+ } from "../chunk-JS6IYB55.js";
4
+ import "../chunk-DOHA5HNJ.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.45.0";
143
+ var version = "3.46.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-2QK37BNF.js";
4
- import "../chunk-6XNMBNIU.js";
3
+ } from "../chunk-FNOYJEUK.js";
4
+ import "../chunk-JS6IYB55.js";
5
5
  import "../chunk-DAOXTQVS.js";
6
- import "../chunk-E4K4KTSR.js";
6
+ import "../chunk-DOHA5HNJ.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "3.45.0";
2
+ var version = "3.46.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-6XNMBNIU.js";
3
+ } from "./chunk-JS6IYB55.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-E4K4KTSR.js";
3
+ } from "./chunk-DOHA5HNJ.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-E4K4KTSR.js";
3
+ } from "./chunk-DOHA5HNJ.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-G6KYCMUM.js";
3
+ } from "./chunk-TLLUDBO4.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.45.0";
28
+ var version = "3.46.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-E4K4KTSR.js";
3
+ } from "./chunk-DOHA5HNJ.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.45.0";
147
+ var version = "3.46.0";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -58,6 +58,28 @@ type DirectorySource = {
58
58
  loadUsers?: () => Promise<DirectoryEntry[]>;
59
59
  loadTeams?: () => Promise<DirectoryEntry[]>;
60
60
  };
61
+ type SchemaEntry = {
62
+ id: string;
63
+ name?: string;
64
+ format?: string;
65
+ content: string;
66
+ source: {
67
+ provider: string;
68
+ id?: string;
69
+ url?: string;
70
+ ref?: string;
71
+ path?: string;
72
+ [key: string]: unknown;
73
+ };
74
+ };
75
+ type SchemaSource = {
76
+ type: 'schemas';
77
+ name: string;
78
+ canResolve: (id: string) => boolean;
79
+ resolve: (id: string, context?: {
80
+ messageFilePath?: string;
81
+ }) => Promise<SchemaEntry | undefined>;
82
+ };
61
83
  type DirectoryConfig = {
62
84
  /**
63
85
  * External sources that sync users and teams into EventCatalog.
@@ -71,6 +93,12 @@ type DirectoryConfig = {
71
93
  */
72
94
  conflictStrategy?: 'local-wins' | 'source-wins' | 'error';
73
95
  };
96
+ type SchemaConfig = {
97
+ /**
98
+ * External sources that resolve message schema references into the generated schemas collection.
99
+ */
100
+ sources?: SchemaSource[];
101
+ };
74
102
  type AuthConfig = {
75
103
  enabled: boolean;
76
104
  };
@@ -221,6 +249,7 @@ interface Config {
221
249
  domains?: ResourceDependency[];
222
250
  };
223
251
  directory?: DirectoryConfig;
252
+ schemas?: SchemaConfig;
224
253
  mermaid?: {
225
254
  maxTextSize?: number;
226
255
  iconPacks?: string[];
@@ -58,6 +58,28 @@ type DirectorySource = {
58
58
  loadUsers?: () => Promise<DirectoryEntry[]>;
59
59
  loadTeams?: () => Promise<DirectoryEntry[]>;
60
60
  };
61
+ type SchemaEntry = {
62
+ id: string;
63
+ name?: string;
64
+ format?: string;
65
+ content: string;
66
+ source: {
67
+ provider: string;
68
+ id?: string;
69
+ url?: string;
70
+ ref?: string;
71
+ path?: string;
72
+ [key: string]: unknown;
73
+ };
74
+ };
75
+ type SchemaSource = {
76
+ type: 'schemas';
77
+ name: string;
78
+ canResolve: (id: string) => boolean;
79
+ resolve: (id: string, context?: {
80
+ messageFilePath?: string;
81
+ }) => Promise<SchemaEntry | undefined>;
82
+ };
61
83
  type DirectoryConfig = {
62
84
  /**
63
85
  * External sources that sync users and teams into EventCatalog.
@@ -71,6 +93,12 @@ type DirectoryConfig = {
71
93
  */
72
94
  conflictStrategy?: 'local-wins' | 'source-wins' | 'error';
73
95
  };
96
+ type SchemaConfig = {
97
+ /**
98
+ * External sources that resolve message schema references into the generated schemas collection.
99
+ */
100
+ sources?: SchemaSource[];
101
+ };
74
102
  type AuthConfig = {
75
103
  enabled: boolean;
76
104
  };
@@ -221,6 +249,7 @@ interface Config {
221
249
  domains?: ResourceDependency[];
222
250
  };
223
251
  directory?: DirectoryConfig;
252
+ schemas?: SchemaConfig;
224
253
  mermaid?: {
225
254
  maxTextSize?: number;
226
255
  iconPacks?: string[];
@@ -1,7 +1,8 @@
1
1
  import {
2
- runMigrations
3
- } from "./chunk-XUAF2H54.js";
4
- import "./chunk-CA4U2JP7.js";
2
+ log_build_default
3
+ } from "./chunk-FNOYJEUK.js";
4
+ import "./chunk-JS6IYB55.js";
5
+ import "./chunk-DAOXTQVS.js";
5
6
  import {
6
7
  resolve_catalog_dependencies_default
7
8
  } from "./chunk-LHR4G2UO.js";
@@ -12,10 +13,9 @@ import {
12
13
  watch
13
14
  } from "./chunk-3H2RT3CM.js";
14
15
  import {
15
- log_build_default
16
- } from "./chunk-2QK37BNF.js";
17
- import "./chunk-6XNMBNIU.js";
18
- import "./chunk-DAOXTQVS.js";
16
+ runMigrations
17
+ } from "./chunk-XUAF2H54.js";
18
+ import "./chunk-CA4U2JP7.js";
19
19
  import {
20
20
  catalogToAstro
21
21
  } from "./chunk-4SMA4HQ3.js";
@@ -28,13 +28,13 @@ import {
28
28
  } from "./chunk-B7HCX5HM.js";
29
29
  import {
30
30
  generate
31
- } from "./chunk-KHL25572.js";
31
+ } from "./chunk-X4AESI6E.js";
32
32
  import {
33
33
  logger
34
- } from "./chunk-G6KYCMUM.js";
34
+ } from "./chunk-TLLUDBO4.js";
35
35
  import {
36
36
  VERSION
37
- } from "./chunk-E4K4KTSR.js";
37
+ } from "./chunk-DOHA5HNJ.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.45.0";
111
+ var version = "3.46.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-KHL25572.js";
4
- import "./chunk-G6KYCMUM.js";
5
- import "./chunk-E4K4KTSR.js";
3
+ } from "./chunk-X4AESI6E.js";
4
+ import "./chunk-TLLUDBO4.js";
5
+ import "./chunk-DOHA5HNJ.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.45.0";
39
+ var version = "3.46.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-G6KYCMUM.js";
4
- import "../chunk-E4K4KTSR.js";
3
+ } from "../chunk-TLLUDBO4.js";
4
+ import "../chunk-DOHA5HNJ.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -19,6 +19,11 @@ import rehypeExpressiveCode from 'rehype-expressive-code';
19
19
  /** @type {import('bin/eventcatalog.config').Config} */
20
20
  import config from './eventcatalog.config';
21
21
  import expressiveCode from 'astro-expressive-code';
22
+ // Expressive Code options (including the non-serializable `themeCssSelector`
23
+ // function) live in ec.config.mjs. The `expressiveCode()` integration loads that
24
+ // file automatically; the rehype plugin used by mdx() below needs it passed
25
+ // explicitly, so we import it here to keep a single source of truth.
26
+ import expressiveCodeConfig from './ec.config.mjs';
22
27
  import ecstudioWatcher from './integrations/ecstudio-watcher.mjs';
23
28
  import eventCatalogIntegration from './src/enterprise/integrations/eventcatalog-features.ts';
24
29
 
@@ -30,13 +35,6 @@ const isDevMode = process.env.EVENTCATALOG_DEV_MODE === 'true';
30
35
  const effectiveOutput = isDevMode ? 'server' : config.output || 'static';
31
36
  const searchType = config.search?.type || 'resource';
32
37
 
33
- const expressiveCodeConfig = {
34
- themes: ['github-light', 'github-dark'],
35
- defaultProps: {
36
- wrap: true,
37
- },
38
- };
39
-
40
38
  const markdownRemarkPlugins = [remarkDirective, remarkDirectives, remarkComment, mermaid, plantuml];
41
39
  const mdxRemarkPlugins = [...markdownRemarkPlugins, remarkResourceRef];
42
40
 
@@ -78,9 +76,8 @@ export default defineConfig({
78
76
  devToolbar: { enabled: false },
79
77
  integrations: [
80
78
  react(),
81
- expressiveCode({
82
- ...expressiveCodeConfig,
83
- }),
79
+ // Options are loaded automatically from ec.config.mjs.
80
+ expressiveCode(),
84
81
  mdx({
85
82
  // https://docs.astro.build/en/guides/integrations-guide/mdx/#optimize
86
83
  optimize: config.mdxOptimize || false,
@@ -0,0 +1,20 @@
1
+ import { defineEcConfig } from 'astro-expressive-code';
2
+
3
+ // Expressive Code configuration lives in this dedicated file (rather than inline
4
+ // in astro.config.mjs) because it contains non-serializable options such as the
5
+ // `themeCssSelector` function. Astro's `<Code>` component (used on the print
6
+ // pages) requires Expressive Code options coming from the Astro config to be
7
+ // JSON-serializable, so functions must be defined here instead.
8
+ //
9
+ // `useDarkModeMediaQuery: false` + `themeCssSelector` make code blocks follow
10
+ // EventCatalog's own `data-theme` toggle instead of the OS `prefers-color-scheme`
11
+ // media query, so they stay readable when the catalog theme and the OS
12
+ // preference disagree (see issue #2607).
13
+ export default defineEcConfig({
14
+ themes: ['github-light', 'github-dark'],
15
+ useDarkModeMediaQuery: false,
16
+ themeCssSelector: (theme) => `[data-theme='${theme.type}']`,
17
+ defaultProps: {
18
+ wrap: true,
19
+ },
20
+ });
@@ -5,7 +5,7 @@ const SchemaViewerPortal = (props: any) => {
5
5
 
6
6
  return (
7
7
  <div
8
- id={`${props.id}-${props.file}-SchemaViewer-portal`}
8
+ data-schema-viewer-portal-id={props.id}
9
9
  data-expand={expandBool ? 'true' : 'false'}
10
10
  data-max-height={props.maxHeight}
11
11
  data-search={searchBool ? 'true' : 'false'}
@@ -1,13 +1,13 @@
1
1
  ---
2
- const { id, filePath } = Astro.props;
2
+ const { id, filePath, collection, version } = Astro.props;
3
3
  import fs from 'node:fs/promises';
4
- import { existsSync } from 'fs';
5
- import yaml from 'js-yaml';
4
+ import { getCollection } from 'astro:content';
6
5
  import JSONSchemaViewer from '@components/SchemaExplorer/JSONSchemaViewer';
7
6
  import AvroSchemaViewer from '@components/SchemaExplorer/AvroSchemaViewer';
8
7
  import Admonition from '../Admonition';
9
8
  import { getMDXComponentsByName } from '@utils/markdown';
10
- import { getAbsoluteFilePathForAstroFile, resolveProjectPath, isAvroSchema } from '@utils/files';
9
+ import { resolveProjectPath } from '@utils/files';
10
+ import { resolveSchemaViewer } from './schema-viewer-utils';
11
11
 
12
12
  let schemas = [];
13
13
 
@@ -15,45 +15,11 @@ try {
15
15
  const absoluteFilePath = resolveProjectPath(filePath);
16
16
  const file = await fs.readFile(absoluteFilePath, 'utf-8');
17
17
  const schemaViewers = getMDXComponentsByName(file, 'SchemaViewer');
18
+ const collectionSchemas = collection && version ? await getCollection('schemas') : [];
18
19
 
19
- // Loop around all the possible SchemaViewers in the file.
20
- const getAllComponents = schemaViewers.map(async (schemaViewerProps: any, index: number) => {
21
- const schemaPath = getAbsoluteFilePathForAstroFile(filePath, schemaViewerProps.file);
22
- const exists = existsSync(schemaPath);
23
- let schema;
24
- let render = true;
25
- let isAvro = false;
26
-
27
- if (exists) {
28
- // Check if this is an Avro schema file
29
- isAvro = isAvroSchema(schemaPath);
30
-
31
- // Load the schema for the component
32
- schema = await fs.readFile(schemaPath, 'utf-8');
33
-
34
- if (schemaPath.endsWith('.yml') || schemaPath.endsWith('.yaml')) {
35
- schema = yaml.load(schema);
36
- } else {
37
- schema = JSON.parse(schema);
38
-
39
- // For non-Avro schemas, let JSON schema control if the component should be rendered
40
- if (!isAvro && schema['x-eventcatalog-render-schema-viewer'] !== undefined) {
41
- render = schema['x-eventcatalog-render-schema-viewer'];
42
- }
43
- }
44
- }
45
-
46
- return {
47
- id: schemaViewerProps.id || id,
48
- exists,
49
- schema,
50
- schemaPath,
51
- isAvroSchema: isAvro,
52
- ...schemaViewerProps,
53
- render,
54
- index,
55
- };
56
- });
20
+ const getAllComponents = schemaViewers.map((schemaViewerProps: any, index: number) =>
21
+ resolveSchemaViewer({ id, version, collection, filePath, schemaViewerProps, collectionSchemas, index })
22
+ );
57
23
 
58
24
  schemas = await Promise.all(getAllComponents);
59
25
  } catch (error) {
@@ -71,8 +37,10 @@ try {
71
37
  <div>
72
38
  {schema.exists && (
73
39
  <div
74
- id={`${schema.id}-${schema.file}-SchemaViewer-client`}
40
+ id={`${schema.id}-${schema.index}-SchemaViewer-client`}
75
41
  class="not-prose my-4"
42
+ data-schema-viewer-client-id={schema.id}
43
+ data-schema-viewer-index={schema.index}
76
44
  data-expand={schema.expand ? 'true' : 'false'}
77
45
  data-search={schema.search !== false ? 'true' : 'false'}
78
46
  >
@@ -105,7 +73,11 @@ try {
105
73
  <Admonition type="warning">
106
74
  <div>
107
75
  <span class="block font-bold">{`<SchemaViewer/>`} failed to load</span>
108
- <span class="block">Tried to load schema from {schema.schemaPath}, but no schema can be found</span>
76
+ <span class="block">
77
+ {schema.parseError
78
+ ? `Tried to parse schema from ${schema.schemaPath}, but it failed: ${schema.parseError}`
79
+ : `Tried to load schema from ${schema.schemaPath || schema.file || 'the message schema collection'}, but no schema can be found`}
80
+ </span>
109
81
  </div>
110
82
  </Admonition>
111
83
  )}
@@ -120,9 +92,9 @@ try {
120
92
  // and then we can move the SchemaViewerClient to that container?
121
93
 
122
94
  function moveSchemaViewerToPortal(schema) {
123
- const portalId = `${schema.id}-${schema.file}-SchemaViewer-portal`;
124
- const schemaViewerContainer = document.getElementById(portalId);
125
- const schemaViewerClient = document.getElementById(`${schema.id}-${schema.file}-SchemaViewer-client`);
95
+ const schemaViewerContainers = document.querySelectorAll(`[data-schema-viewer-portal-id="${schema.id}"]`);
96
+ const schemaViewerContainer = schemaViewerContainers[schema.index];
97
+ const schemaViewerClient = document.getElementById(`${schema.id}-${schema.index}-SchemaViewer-client`);
126
98
 
127
99
  if (schemaViewerContainer && schemaViewerClient) {
128
100
  // Get attributes from the portal
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveSchemaViewer } from './schema-viewer-utils';
3
+
4
+ const currentMessageSchema = {
5
+ id: 'schema:events:OrderPlaced:1.0.0:git://contracts/events/OrderPlaced.schema.json',
6
+ data: {
7
+ ref: 'git://contracts/events/OrderPlaced.schema.json',
8
+ format: 'jsonschema',
9
+ content: '{"type":"object","title":"OrderPlaced"}',
10
+ default: true,
11
+ message: {
12
+ collection: 'events',
13
+ id: 'OrderPlaced',
14
+ version: '1.0.0',
15
+ },
16
+ },
17
+ };
18
+
19
+ describe('resolveSchemaViewer', () => {
20
+ it('uses the current message default schema when no file is set', async () => {
21
+ const schema = await resolveSchemaViewer({
22
+ id: 'OrderPlaced',
23
+ version: '1.0.0',
24
+ collection: 'events',
25
+ filePath: 'events/OrderPlaced/index.mdx',
26
+ schemaViewerProps: {},
27
+ collectionSchemas: [currentMessageSchema],
28
+ index: 0,
29
+ });
30
+
31
+ expect(schema.exists).toBe(true);
32
+ expect(schema.schema.title).toBe('OrderPlaced');
33
+ expect(schema.schemaPath).toBe('git://contracts/events/OrderPlaced.schema.json');
34
+ });
35
+
36
+ it('does not use the schema collection when a file is set and missing', async () => {
37
+ const schema = await resolveSchemaViewer({
38
+ id: 'OrderPlaced',
39
+ version: '1.0.0',
40
+ collection: 'events',
41
+ filePath: 'events/OrderPlaced/index.mdx',
42
+ schemaViewerProps: {
43
+ file: 'missing-schema.json',
44
+ },
45
+ collectionSchemas: [currentMessageSchema],
46
+ index: 0,
47
+ });
48
+
49
+ expect(schema.exists).toBe(false);
50
+ expect(schema.schema).toBeUndefined();
51
+ expect(schema.schemaKey).toBe('missing-schema.json');
52
+ });
53
+ });
@@ -0,0 +1,133 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { load as loadYaml } from 'js-yaml';
4
+ import { getAbsoluteFilePathForAstroFile, isAvroSchema } from '../../../utils/files';
5
+
6
+ type SchemaViewerProps = {
7
+ id?: string;
8
+ file?: string;
9
+ [key: string]: any;
10
+ };
11
+
12
+ type CollectionSchema = {
13
+ id: string;
14
+ data: {
15
+ content?: string;
16
+ file?: string;
17
+ filePath?: string;
18
+ ref?: string;
19
+ format?: string;
20
+ default?: boolean;
21
+ source?: {
22
+ path?: string;
23
+ branch?: string;
24
+ };
25
+ message?: {
26
+ collection: string;
27
+ id: string;
28
+ version: string;
29
+ };
30
+ };
31
+ };
32
+
33
+ type ResolveSchemaViewerOptions = {
34
+ id: string;
35
+ version?: string;
36
+ collection?: string;
37
+ filePath: string;
38
+ schemaViewerProps: SchemaViewerProps;
39
+ collectionSchemas: CollectionSchema[];
40
+ index: number;
41
+ };
42
+
43
+ export const getSchemaViewerKey = (schemaViewerProps: SchemaViewerProps) => schemaViewerProps.file || 'default';
44
+
45
+ const isYamlSchema = (schemaPath?: string, format?: string) =>
46
+ schemaPath?.endsWith('.yml') || schemaPath?.endsWith('.yaml') || format === 'yaml';
47
+
48
+ const isAvroSchemaReference = (schemaPath?: string, format?: string) =>
49
+ format === 'avro' || (schemaPath ? isAvroSchema(schemaPath) : false);
50
+
51
+ const parseSchemaContent = (content: string, schemaPath?: string, format?: string) => {
52
+ if (isYamlSchema(schemaPath, format)) return loadYaml(content);
53
+ return JSON.parse(content);
54
+ };
55
+
56
+ const shouldRenderSchema = (schema: any, isAvro: boolean) => {
57
+ if (isAvro) return true;
58
+ return schema?.['x-eventcatalog-render-schema-viewer'] !== undefined ? schema['x-eventcatalog-render-schema-viewer'] : true;
59
+ };
60
+
61
+ const getCollectionSchemaForViewer = ({
62
+ collectionSchemas,
63
+ collection,
64
+ id,
65
+ version,
66
+ }: Pick<ResolveSchemaViewerOptions, 'collectionSchemas' | 'collection' | 'id' | 'version'>) => {
67
+ if (!collection || !version) return undefined;
68
+
69
+ const resourceSchemas = collectionSchemas.filter((schema) => {
70
+ const message = schema.data.message;
71
+ if (!message) return false;
72
+ return message.collection === collection && message.id === id && message.version === version;
73
+ });
74
+
75
+ return resourceSchemas.find((schema) => schema.data.default) || resourceSchemas[0];
76
+ };
77
+
78
+ const getCollectionSchemaPath = (schema?: CollectionSchema) =>
79
+ schema?.data.filePath || schema?.data.file || schema?.data.source?.path || schema?.data.ref;
80
+
81
+ export const resolveSchemaViewer = async ({
82
+ id,
83
+ version,
84
+ collection,
85
+ filePath,
86
+ schemaViewerProps,
87
+ collectionSchemas,
88
+ index,
89
+ }: ResolveSchemaViewerOptions) => {
90
+ const schemaKey = getSchemaViewerKey(schemaViewerProps);
91
+ const localSchemaPath = schemaViewerProps.file ? getAbsoluteFilePathForAstroFile(filePath, schemaViewerProps.file) : undefined;
92
+ const localSchemaExists = localSchemaPath ? existsSync(localSchemaPath) : false;
93
+
94
+ let schema;
95
+ let render = true;
96
+ let isAvro = false;
97
+ let schemaPath = localSchemaPath;
98
+ let parseError;
99
+
100
+ if (localSchemaExists && localSchemaPath) {
101
+ isAvro = isAvroSchema(localSchemaPath);
102
+ const content = await readFile(localSchemaPath, 'utf-8');
103
+ schema = parseSchemaContent(content, localSchemaPath);
104
+ render = shouldRenderSchema(schema, isAvro);
105
+ } else if (!schemaViewerProps.file) {
106
+ const collectionSchema = getCollectionSchemaForViewer({ collectionSchemas, collection, id, version });
107
+ const content = collectionSchema?.data.content;
108
+ schemaPath = getCollectionSchemaPath(collectionSchema);
109
+
110
+ if (content) {
111
+ try {
112
+ isAvro = isAvroSchemaReference(schemaPath, collectionSchema?.data.format);
113
+ schema = parseSchemaContent(content, schemaPath, collectionSchema?.data.format);
114
+ render = shouldRenderSchema(schema, isAvro);
115
+ } catch (error) {
116
+ parseError = error instanceof Error ? error.message : 'Unknown parsing error';
117
+ }
118
+ }
119
+ }
120
+
121
+ return {
122
+ id: schemaViewerProps.id || id,
123
+ exists: localSchemaExists || schema !== undefined,
124
+ schema,
125
+ schemaPath,
126
+ schemaKey,
127
+ isAvroSchema: isAvro,
128
+ parseError,
129
+ ...schemaViewerProps,
130
+ render,
131
+ index,
132
+ };
133
+ };
@@ -71,6 +71,7 @@ const channelPointer = z
71
71
 
72
72
  const schemaPointer = z.object({
73
73
  id: z.string().optional(),
74
+ ref: z.string().optional(),
74
75
  file: z.string().optional(),
75
76
  path: z.string().optional(),
76
77
  name: z.string().optional(),
@@ -1018,10 +1019,13 @@ const schemas = defineCollection({
1018
1019
  ]) as string[],
1019
1020
  base: projectDirBase,
1020
1021
  },
1022
+ sources: config.schemas?.sources ?? [],
1021
1023
  }),
1022
1024
  schema: z
1023
1025
  .object({
1024
1026
  format: z.string(),
1027
+ schemaId: z.string().optional(),
1028
+ ref: z.string().optional(),
1025
1029
  content: z.string().optional(),
1026
1030
  file: z.string().optional(),
1027
1031
  filePath: z.string().optional(),
@@ -1038,8 +1042,11 @@ const schemas = defineCollection({
1038
1042
  }),
1039
1043
  source: z.object({
1040
1044
  provider: z.string(),
1045
+ id: z.string().optional(),
1041
1046
  path: z.string().optional(),
1042
1047
  url: z.string().optional(),
1048
+ ref: z.string().optional(),
1049
+ branch: z.string().optional(),
1043
1050
  }),
1044
1051
  readOnly: z.boolean().optional(),
1045
1052
  })
@@ -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 &&
@@ -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 && {
@@ -67,6 +67,7 @@ export type ResourceGroupContext = {
67
67
  entities?: CollectionEntry<'entities'>[];
68
68
  dataProducts: CollectionEntry<'data-products'>[];
69
69
  diagrams: CollectionEntry<'diagrams'>[];
70
+ schemas?: CollectionEntry<'schemas'>[];
70
71
  adrs: Adr[];
71
72
  users?: CollectionEntry<'users'>[];
72
73
  teams?: CollectionEntry<'teams'>[];
@@ -292,6 +292,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
292
292
  dataProducts,
293
293
  entities,
294
294
  adrs,
295
+ schemas,
295
296
  resourceDocs,
296
297
  resourceDocCategories,
297
298
  ] = await Promise.all([
@@ -309,6 +310,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
309
310
  getDataProducts({ getAllVersions: false }),
310
311
  getEntities({ getAllVersions: false }),
311
312
  getAdrs({ getAllVersions: false }),
313
+ getCollection('schemas'),
312
314
  getResourceDocs(),
313
315
  getResourceDocCategories(),
314
316
  ]);
@@ -330,6 +332,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
330
332
  containers,
331
333
  channels,
332
334
  diagrams,
335
+ schemas,
333
336
  dataProducts,
334
337
  entities,
335
338
  adrs,
@@ -4,12 +4,18 @@ import matter from 'gray-matter';
4
4
  import fs from 'node:fs/promises';
5
5
  import fsSync from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import pc from 'picocolors';
9
+ import { isEventCatalogScaleEnabled } from '../feature';
7
10
  import { sortVersioned } from './util';
8
11
 
12
+ const colors = pc.createColors(true);
13
+
9
14
  type MessageCollection = 'events' | 'commands' | 'queries';
10
15
 
11
16
  type SchemaReference = {
12
17
  id?: string;
18
+ ref?: string;
13
19
  file?: string;
14
20
  path?: string;
15
21
  name?: string;
@@ -18,6 +24,34 @@ type SchemaReference = {
18
24
  default?: boolean;
19
25
  };
20
26
 
27
+ type SchemaSourceEntry = {
28
+ provider: string;
29
+ id?: string;
30
+ path?: string;
31
+ url?: string;
32
+ ref?: string;
33
+ [key: string]: unknown;
34
+ };
35
+
36
+ type SchemaSource = {
37
+ type: 'schemas';
38
+ name: string;
39
+ canResolve: (ref: string) => boolean;
40
+ resolve: (
41
+ ref: string,
42
+ context?: { messageFilePath?: string }
43
+ ) => Promise<
44
+ | {
45
+ id: string;
46
+ name?: string;
47
+ format?: string;
48
+ content: string;
49
+ source: SchemaSourceEntry;
50
+ }
51
+ | undefined
52
+ >;
53
+ };
54
+
21
55
  type MessageFrontmatter = {
22
56
  id?: string;
23
57
  name?: string;
@@ -30,6 +64,8 @@ type MessageFrontmatter = {
30
64
 
31
65
  export type MessageSchemaResource = {
32
66
  id: string;
67
+ schemaId?: string;
68
+ ref?: string;
33
69
  name?: string;
34
70
  version?: string;
35
71
  format: string;
@@ -48,8 +84,19 @@ export type MessageSchemaResource = {
48
84
  owners?: string[];
49
85
  };
50
86
  source: {
51
- provider: 'file';
52
- path: string;
87
+ provider: string;
88
+ id?: string;
89
+ path?: string;
90
+ url?: string;
91
+ ref?: string;
92
+ [key: string]: unknown;
93
+ };
94
+ readOnly?: boolean;
95
+ };
96
+
97
+ type InternalMessageSchemaResource = MessageSchemaResource & {
98
+ _context?: {
99
+ messageFilePath?: string;
53
100
  };
54
101
  };
55
102
 
@@ -58,11 +105,13 @@ type SchemaLoaderOptions = {
58
105
  pattern: string[];
59
106
  base?: string;
60
107
  };
108
+ sources?: SchemaSource[];
61
109
  };
62
110
 
63
111
  type LoaderContext = Parameters<Loader['load']>[0];
64
112
 
65
113
  const MESSAGE_COLLECTIONS: MessageCollection[] = ['events', 'commands', 'queries'];
114
+ const FILE_SCHEMA_REF_PREFIX = 'file://';
66
115
 
67
116
  const normalizePath = (value: string) => value.replace(/\\/g, '/');
68
117
 
@@ -85,11 +134,137 @@ const getSchemaFormat = (schemaPath: string) => {
85
134
  const buildGeneratedSchemaId = (message: { collection: MessageCollection; id: string; version: string }, schemaPath: string) =>
86
135
  `schema:${message.collection}:${message.id}:${message.version}:${schemaPath}`;
87
136
 
137
+ const getSchemaCollectionId = ({
138
+ message,
139
+ reference,
140
+ schemaRef,
141
+ schemaFile,
142
+ }: {
143
+ message: { collection: MessageCollection; id: string; version: string };
144
+ reference: SchemaReference;
145
+ schemaRef?: string;
146
+ schemaFile?: string;
147
+ }) => {
148
+ if (schemaRef) return buildGeneratedSchemaId(message, schemaRef);
149
+ if (reference.id) return buildGeneratedSchemaId(message, reference.id);
150
+ return buildGeneratedSchemaId(message, schemaFile as string);
151
+ };
152
+
88
153
  const getSchemaFile = (reference: SchemaReference) => reference.file ?? reference.path;
89
154
 
155
+ const getSchemaRef = (reference: SchemaReference) => {
156
+ if (reference.ref) return reference.ref;
157
+ if (!getSchemaFile(reference)) return reference.id;
158
+ return undefined;
159
+ };
160
+
161
+ const getSchemaDisplayName = ({
162
+ referenceName,
163
+ resolvedName,
164
+ schemaFile,
165
+ filePath,
166
+ schemaRef,
167
+ sourcePath,
168
+ schemaId,
169
+ messageName,
170
+ }: {
171
+ referenceName?: string;
172
+ resolvedName?: string;
173
+ schemaFile?: string;
174
+ filePath?: string;
175
+ schemaRef?: string;
176
+ sourcePath?: string;
177
+ schemaId: string;
178
+ messageName?: string;
179
+ }) => {
180
+ const pathLikeName = schemaFile ?? filePath ?? sourcePath ?? schemaRef ?? schemaId;
181
+ return referenceName ?? resolvedName ?? path.basename(pathLikeName) ?? messageName ?? 'Schema';
182
+ };
183
+
90
184
  const schemaFileExists = (schema: MessageSchemaResource): schema is MessageSchemaResource & { filePath: string } =>
91
185
  Boolean(schema.filePath && fsSync.existsSync(schema.filePath));
92
186
 
187
+ const getSchemaProvider = (id: string) => {
188
+ try {
189
+ const url = new URL(id);
190
+ return url.protocol.replace(':', '') || 'external';
191
+ } catch {
192
+ return 'external';
193
+ }
194
+ };
195
+
196
+ const isFileSchemaRef = (ref?: string) => ref?.startsWith(FILE_SCHEMA_REF_PREFIX);
197
+
198
+ const getFileSchemaRefPath = (ref: string, messageFilePath: string) => {
199
+ if (ref.startsWith('file:///') || ref.startsWith('file://localhost/')) {
200
+ try {
201
+ const filePath = fileURLToPath(ref);
202
+ return {
203
+ filePath,
204
+ sourcePath: filePath,
205
+ };
206
+ } catch {
207
+ throw new Error(`Invalid file schema ref "${ref}". Expected file://<relative-path> or file:///absolute/path.`);
208
+ }
209
+ }
210
+
211
+ const sourcePath = decodeURIComponent(ref.slice(FILE_SCHEMA_REF_PREFIX.length));
212
+ if (!sourcePath) {
213
+ throw new Error(`Invalid file schema ref "${ref}". Expected file://<relative-path> or file:///absolute/path.`);
214
+ }
215
+
216
+ return {
217
+ filePath: path.resolve(path.dirname(messageFilePath), sourcePath),
218
+ sourcePath,
219
+ };
220
+ };
221
+
222
+ const getTimestamp = () => {
223
+ const now = new Date();
224
+ return now.toLocaleTimeString('en-US', { hour12: false });
225
+ };
226
+
227
+ const logSchemaInfo = (message: string) => {
228
+ console.log(`${colors.dim(getTimestamp())} ${colors.blue('[schemas]')} ${message}`);
229
+ };
230
+
231
+ const getMessageTypeLabel = (collection: MessageCollection) => {
232
+ if (collection === 'events') return 'event';
233
+ if (collection === 'commands') return 'command';
234
+ return 'query';
235
+ };
236
+
237
+ const getErrorMessage = (error: unknown) => {
238
+ return error instanceof Error ? error.message : String(error);
239
+ };
240
+
241
+ const getSchemaResolveRef = (schema: MessageSchemaResource) => schema.ref ?? schema.id;
242
+
243
+ const buildSchemaSourceErrorMessage = ({
244
+ schema,
245
+ source,
246
+ error,
247
+ }: {
248
+ schema: MessageSchemaResource;
249
+ source: SchemaSource;
250
+ error: unknown;
251
+ }) => {
252
+ const messageType = getMessageTypeLabel(schema.message.collection);
253
+
254
+ return [
255
+ '',
256
+ colors.red(colors.bold('[schemas] Failed to resolve schema')),
257
+ '',
258
+ ` Message: ${messageType} "${schema.message.id}" version "${schema.message.version}"`,
259
+ ` Schema: ${getSchemaResolveRef(schema)}`,
260
+ ` Source: ${source.name}`,
261
+ '',
262
+ colors.bold('Reason:'),
263
+ ` ${getErrorMessage(error)}`,
264
+ '',
265
+ ].join('\n');
266
+ };
267
+
93
268
  const addLatestMetadata = (schemas: MessageSchemaResource[]) => {
94
269
  const schemasByMessage = schemas.reduce(
95
270
  (acc, schema) => {
@@ -138,11 +313,10 @@ export const getMessageSchemasFromFrontmatter = ({
138
313
  };
139
314
 
140
315
  const schemaReferences =
141
- data.schemas?.filter((schema) => getSchemaFile(schema)) ??
316
+ data.schemas?.filter((schema) => getSchemaRef(schema) || getSchemaFile(schema)) ??
142
317
  (data.schemaPath
143
318
  ? [
144
319
  {
145
- id: buildGeneratedSchemaId(message, data.schemaPath),
146
320
  file: data.schemaPath,
147
321
  name: 'Schema',
148
322
  default: true,
@@ -151,29 +325,123 @@ export const getMessageSchemasFromFrontmatter = ({
151
325
  : []);
152
326
 
153
327
  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);
328
+ const schemaFile = getSchemaFile(reference);
329
+ const schemaRef = getSchemaRef(reference);
330
+ const fileSchemaRef = isFileSchemaRef(schemaRef) ? getFileSchemaRefPath(schemaRef as string, messageFilePath) : undefined;
331
+ const schemaFilePath = schemaFile ? path.resolve(path.dirname(messageFilePath), schemaFile) : fileSchemaRef?.filePath;
332
+ const schemaId = getSchemaCollectionId({ message, reference, schemaRef, schemaFile });
333
+ const schemaPathForFormat = schemaFile ?? fileSchemaRef?.filePath;
157
334
 
158
335
  return {
159
336
  id: schemaId,
160
- name: reference.name ?? schemaId,
337
+ ...(reference.id && !schemaRef ? { schemaId: reference.id } : {}),
338
+ ...(schemaRef ? { ref: schemaRef } : {}),
339
+ name: getSchemaDisplayName({
340
+ referenceName: reference.name,
341
+ schemaFile,
342
+ filePath: fileSchemaRef?.filePath,
343
+ schemaRef,
344
+ schemaId,
345
+ messageName: data.name,
346
+ }),
161
347
  version: data.version,
162
- format: reference.format ?? getSchemaFormat(schemaFile),
348
+ format: reference.format ?? (schemaPathForFormat ? getSchemaFormat(schemaPathForFormat) : 'unknown'),
163
349
  file: schemaFile,
164
350
  filePath: schemaFilePath,
165
351
  environments: reference.environments,
166
352
  default: reference.default,
167
353
  message,
168
354
  source: {
169
- provider: 'file',
170
- path: schemaFile,
355
+ provider: schemaFile || fileSchemaRef ? 'file' : getSchemaProvider(schemaRef ?? schemaId),
356
+ path: schemaFile ?? fileSchemaRef?.sourcePath,
171
357
  },
172
358
  };
173
359
  });
174
360
  };
175
361
 
176
- export const loadMessageSchemas = async ({ pattern, base }: SchemaLoaderOptions['messages']) => {
362
+ const resolveSchemaSource = async (
363
+ schema: InternalMessageSchemaResource,
364
+ source: SchemaSource
365
+ ): Promise<InternalMessageSchemaResource | undefined> => {
366
+ if (schemaFileExists(schema)) return schema;
367
+ if (schema.filePath) return undefined;
368
+
369
+ let resolvedSchema: Awaited<ReturnType<SchemaSource['resolve']>>;
370
+ const schemaResolveRef = getSchemaResolveRef(schema);
371
+
372
+ try {
373
+ resolvedSchema = await source.resolve(schemaResolveRef, schema._context);
374
+ } catch (error) {
375
+ throw new Error(buildSchemaSourceErrorMessage({ schema, source, error }));
376
+ }
377
+
378
+ if (!resolvedSchema) return undefined;
379
+
380
+ const resolvedMessageSchema: InternalMessageSchemaResource = {
381
+ ...schema,
382
+ format: schema.format !== 'unknown' ? schema.format : (resolvedSchema.format ?? 'unknown'),
383
+ content: resolvedSchema.content,
384
+ source: resolvedSchema.source,
385
+ readOnly: true,
386
+ };
387
+
388
+ resolvedMessageSchema.name = getSchemaDisplayName({
389
+ referenceName: schema.name,
390
+ resolvedName: resolvedSchema.name,
391
+ schemaRef: schema.ref,
392
+ sourcePath: resolvedSchema.source.path,
393
+ schemaId: schema.id,
394
+ messageName: schema.message.name,
395
+ });
396
+
397
+ return resolvedMessageSchema;
398
+ };
399
+
400
+ const resolveSchemaSources = async (schemas: InternalMessageSchemaResource[], sources: SchemaSource[] = []) => {
401
+ if (sources.length > 0 && !isEventCatalogScaleEnabled()) {
402
+ throw new Error('Schema sources require EventCatalog Scale.');
403
+ }
404
+
405
+ const localSchemas = schemas.filter(schemaFileExists);
406
+ const externalSchemas = schemas.filter((schema) => !schema.filePath);
407
+ const resolvedExternalSchemas: InternalMessageSchemaResource[] = [];
408
+ const resolvedExternalSchemaIds = new Set<string>();
409
+
410
+ for (const source of sources) {
411
+ const sourceSchemas = externalSchemas.filter((schema) => source.canResolve(getSchemaResolveRef(schema)));
412
+ if (sourceSchemas.length === 0) continue;
413
+
414
+ logSchemaInfo(
415
+ `Loading ${sourceSchemas.length} schema${sourceSchemas.length === 1 ? '' : 's'} from schema source "${source.name}"`
416
+ );
417
+
418
+ const resolvedSchemas = await Promise.all(sourceSchemas.map((schema) => resolveSchemaSource(schema, source)));
419
+ const syncedSchemas = resolvedSchemas.filter((schema): schema is InternalMessageSchemaResource => schema !== undefined);
420
+
421
+ for (const schema of syncedSchemas) {
422
+ resolvedExternalSchemaIds.add(schema.id);
423
+ resolvedExternalSchemas.push(schema);
424
+ }
425
+
426
+ const skippedSchemas = sourceSchemas.length - syncedSchemas.length;
427
+ logSchemaInfo(
428
+ `Synced ${syncedSchemas.length} schema${syncedSchemas.length === 1 ? '' : 's'} from schema source "${source.name}"${
429
+ skippedSchemas > 0 ? ` (${skippedSchemas} skipped)` : ''
430
+ }`
431
+ );
432
+ }
433
+
434
+ return [
435
+ ...localSchemas,
436
+ ...resolvedExternalSchemas,
437
+ ...schemas.filter((schema) => schema.filePath && !schemaFileExists(schema)),
438
+ ].filter((schema) => {
439
+ if (schema.filePath) return schemaFileExists(schema);
440
+ return resolvedExternalSchemaIds.has(schema.id);
441
+ });
442
+ };
443
+
444
+ const loadMessageSchemaResources = async ({ pattern, base }: SchemaLoaderOptions['messages']) => {
177
445
  if (!base) return [];
178
446
 
179
447
  const files = await glob(pattern, {
@@ -189,14 +457,32 @@ export const loadMessageSchemas = async ({ pattern, base }: SchemaLoaderOptions[
189
457
  if (!collection) return [];
190
458
 
191
459
  const { data } = matter.read(file) as { data: MessageFrontmatter };
192
- return getMessageSchemasFromFrontmatter({ data, collection, messageFilePath: file });
460
+ return getMessageSchemasFromFrontmatter({ data, collection, messageFilePath: file }).map((schema) => ({
461
+ ...schema,
462
+ _context: {
463
+ messageFilePath: file,
464
+ },
465
+ }));
193
466
  })
194
467
  );
195
468
 
196
- return addLatestMetadata(schemas.flat().filter(schemaFileExists));
469
+ return schemas.flat();
470
+ };
471
+
472
+ const stripSchemaLoaderContext = (schema: InternalMessageSchemaResource): MessageSchemaResource => {
473
+ const { _context, ...publicSchema } = schema;
474
+ return publicSchema;
475
+ };
476
+
477
+ export const loadMessageSchemas = async (messages: SchemaLoaderOptions['messages'], sources: SchemaSource[] = []) => {
478
+ const schemas = await loadMessageSchemaResources(messages);
479
+ const resolvedSchemas = await resolveSchemaSources(schemas, sources);
480
+
481
+ return addLatestMetadata(resolvedSchemas.map(stripSchemaLoaderContext));
197
482
  };
198
483
 
199
484
  const getSchemaBody = async (schema: MessageSchemaResource) => {
485
+ if (schema.content !== undefined) return schema.content;
200
486
  if (!schemaFileExists(schema)) return undefined;
201
487
  return fs.readFile(schema.filePath, 'utf8');
202
488
  };
@@ -222,12 +508,12 @@ const setSchema = async (context: LoaderContext, schema: MessageSchemaResource)
222
508
  });
223
509
  };
224
510
 
225
- export const schemaLoader = ({ messages }: SchemaLoaderOptions): Loader => {
511
+ export const schemaLoader = ({ messages, sources = [] }: SchemaLoaderOptions): Loader => {
226
512
  return {
227
513
  name: 'eventcatalog-schema-loader',
228
514
  load: async (context) => {
229
515
  context.store.clear();
230
- const schemas = await loadMessageSchemas(messages);
516
+ const schemas = await loadMessageSchemas(messages, sources);
231
517
 
232
518
  for (const schema of schemas) {
233
519
  await setSchema(context, schema);
@@ -1,4 +1,4 @@
1
- import path from 'node:path';
1
+ import * as path from 'node:path';
2
2
 
3
3
  /**
4
4
  * Resolves a file path relative to PROJECT_DIR, handling ../ paths correctly
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "3.45.0",
10
+ "version": "3.46.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -74,7 +74,7 @@
74
74
  "elkjs": "^0.10.0",
75
75
  "glob": "^13.0.6",
76
76
  "gray-matter": "^4.0.3",
77
- "hono": "4.12.18",
77
+ "hono": "4.12.21",
78
78
  "html-to-image": "^1.11.11",
79
79
  "js-yaml": "^4.1.1",
80
80
  "jsonpath-plus": "^10.4.0",
@@ -112,8 +112,8 @@
112
112
  "update-notifier": "^7.3.1",
113
113
  "uuid": "^10.0.0",
114
114
  "zod": "^4.3.6",
115
- "@eventcatalog/sdk": "2.24.1",
116
115
  "@eventcatalog/linter": "1.0.29",
116
+ "@eventcatalog/sdk": "2.24.1",
117
117
  "@eventcatalog/visualiser": "^3.22.1"
118
118
  },
119
119
  "devDependencies": {