@eventcatalog/core 4.10.15 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) 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/astro-output.cjs +60 -1
  6. package/dist/astro-output.js +1 -1
  7. package/dist/{chunk-U52FCZQ6.js → chunk-2YJRF4ZX.js} +1 -1
  8. package/dist/chunk-634NFV5U.js +75 -0
  9. package/dist/{chunk-I6HUOX4R.js → chunk-AXDNSGAN.js} +1 -1
  10. package/dist/chunk-DHH5U5PF.js +53 -0
  11. package/dist/{chunk-A4JRWRCV.js → chunk-DSCIWCX2.js} +1 -1
  12. package/dist/chunk-FWXEV4LF.js +19 -0
  13. package/dist/{chunk-Q2ATWE7N.js → chunk-GZVDXCYF.js} +1 -1
  14. package/dist/{chunk-7LWOCX4R.js → chunk-RYNPEIHQ.js} +1 -1
  15. package/dist/{chunk-HBVTU6ZP.js → chunk-YYVW4DCO.js} +1 -1
  16. package/dist/constants.cjs +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/copy-core.cjs +88 -0
  19. package/dist/copy-core.d.cts +4 -0
  20. package/dist/copy-core.d.ts +4 -0
  21. package/dist/copy-core.js +8 -0
  22. package/dist/eventcatalog.cjs +234 -115
  23. package/dist/eventcatalog.js +36 -30
  24. package/dist/federation/federate.js +3 -3
  25. package/dist/federation/source-provider.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/update-check.cjs +44 -0
  29. package/dist/update-check.d.cts +18 -0
  30. package/dist/update-check.d.ts +18 -0
  31. package/dist/update-check.js +8 -0
  32. package/dist/utils/cli-logger.cjs +1 -1
  33. package/dist/utils/cli-logger.js +2 -2
  34. package/eventcatalog/astro.config.mjs +8 -1
  35. package/eventcatalog/public/icons/languages/android.svg +1 -0
  36. package/eventcatalog/public/icons/languages/angular.svg +1 -0
  37. package/eventcatalog/public/icons/languages/apple.svg +1 -0
  38. package/eventcatalog/public/icons/languages/arduino.svg +1 -0
  39. package/eventcatalog/public/icons/languages/bash.svg +1 -0
  40. package/eventcatalog/public/icons/languages/bun.svg +1 -0
  41. package/eventcatalog/public/icons/languages/c.svg +1 -0
  42. package/eventcatalog/public/icons/languages/clojure.svg +1 -0
  43. package/eventcatalog/public/icons/languages/cpp.svg +1 -0
  44. package/eventcatalog/public/icons/languages/crystal.svg +1 -0
  45. package/eventcatalog/public/icons/languages/csharp.svg +1 -0
  46. package/eventcatalog/public/icons/languages/css.svg +1 -0
  47. package/eventcatalog/public/icons/languages/curl.svg +1 -0
  48. package/eventcatalog/public/icons/languages/dart.svg +1 -0
  49. package/eventcatalog/public/icons/languages/deno.svg +1 -0
  50. package/eventcatalog/public/icons/languages/docker.svg +1 -0
  51. package/eventcatalog/public/icons/languages/elm.svg +1 -0
  52. package/eventcatalog/public/icons/languages/erlang.svg +1 -0
  53. package/eventcatalog/public/icons/languages/flutter.svg +1 -0
  54. package/eventcatalog/public/icons/languages/fortran.svg +1 -0
  55. package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
  56. package/eventcatalog/public/icons/languages/git.svg +1 -0
  57. package/eventcatalog/public/icons/languages/gradle.svg +1 -0
  58. package/eventcatalog/public/icons/languages/graphql.svg +1 -0
  59. package/eventcatalog/public/icons/languages/grpc.svg +1 -0
  60. package/eventcatalog/public/icons/languages/haskell.svg +1 -0
  61. package/eventcatalog/public/icons/languages/hasura.svg +1 -0
  62. package/eventcatalog/public/icons/languages/html.svg +1 -0
  63. package/eventcatalog/public/icons/languages/httpie.svg +1 -0
  64. package/eventcatalog/public/icons/languages/javascript.svg +1 -0
  65. package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
  66. package/eventcatalog/public/icons/languages/json.svg +1 -0
  67. package/eventcatalog/public/icons/languages/julia.svg +1 -0
  68. package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
  69. package/eventcatalog/public/icons/languages/kafka.svg +1 -0
  70. package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
  71. package/eventcatalog/public/icons/languages/lua.svg +1 -0
  72. package/eventcatalog/public/icons/languages/markdown.svg +1 -0
  73. package/eventcatalog/public/icons/languages/maven.svg +1 -0
  74. package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
  75. package/eventcatalog/public/icons/languages/mysql.svg +1 -0
  76. package/eventcatalog/public/icons/languages/nginx.svg +1 -0
  77. package/eventcatalog/public/icons/languages/nim.svg +1 -0
  78. package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
  79. package/eventcatalog/public/icons/languages/perl.svg +1 -0
  80. package/eventcatalog/public/icons/languages/php.svg +1 -0
  81. package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
  82. package/eventcatalog/public/icons/languages/prisma.svg +1 -0
  83. package/eventcatalog/public/icons/languages/purescript.svg +1 -0
  84. package/eventcatalog/public/icons/languages/python.svg +1 -0
  85. package/eventcatalog/public/icons/languages/r.svg +1 -0
  86. package/eventcatalog/public/icons/languages/react.svg +1 -0
  87. package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
  88. package/eventcatalog/public/icons/languages/redis.svg +1 -0
  89. package/eventcatalog/public/icons/languages/ruby.svg +1 -0
  90. package/eventcatalog/public/icons/languages/rust.svg +1 -0
  91. package/eventcatalog/public/icons/languages/sass.svg +1 -0
  92. package/eventcatalog/public/icons/languages/scala.svg +1 -0
  93. package/eventcatalog/public/icons/languages/solidity.svg +1 -0
  94. package/eventcatalog/public/icons/languages/spring.svg +1 -0
  95. package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
  96. package/eventcatalog/public/icons/languages/svelte.svg +1 -0
  97. package/eventcatalog/public/icons/languages/swagger.svg +1 -0
  98. package/eventcatalog/public/icons/languages/swift.svg +1 -0
  99. package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
  100. package/eventcatalog/public/icons/languages/terraform.svg +1 -0
  101. package/eventcatalog/public/icons/languages/toml.svg +1 -0
  102. package/eventcatalog/public/icons/languages/typescript.svg +1 -0
  103. package/eventcatalog/public/icons/languages/unity.svg +1 -0
  104. package/eventcatalog/public/icons/languages/vim.svg +1 -0
  105. package/eventcatalog/public/icons/languages/vue.svg +1 -0
  106. package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
  107. package/eventcatalog/public/icons/languages/yaml.svg +1 -0
  108. package/eventcatalog/public/icons/languages/zig.svg +1 -0
  109. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
  110. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
  111. package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
  112. package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
  113. package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
  114. package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
  115. package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
  116. package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
  117. package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
  118. package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
  119. package/eventcatalog/src/components/MDX/components.tsx +6 -0
  120. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
  121. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
  122. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
  123. package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
  124. package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
  125. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
  126. package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
  127. package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
  128. package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
  129. package/eventcatalog/src/content.config.ts +28 -0
  130. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
  131. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
  132. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
  133. package/eventcatalog/src/plugins/empty-collection-warning.d.ts +21 -0
  134. package/eventcatalog/src/plugins/empty-collection-warning.mjs +107 -0
  135. package/eventcatalog/src/plugins/quiet-empty-collections-logger.mjs +11 -0
  136. package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
  137. package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
  138. package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
  139. package/eventcatalog/src/utils/collections/channels.ts +12 -4
  140. package/eventcatalog/src/utils/collections/examples.ts +133 -41
  141. package/eventcatalog/src/utils/collections/flows.ts +43 -0
  142. package/eventcatalog/src/utils/collections/messages.ts +2 -2
  143. package/eventcatalog/src/utils/collections/schemas.ts +15 -0
  144. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +17 -15
  145. package/eventcatalog/src/utils/schema-explorer.ts +73 -7
  146. package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
  147. package/package.json +5 -4
  148. package/dist/chunk-RDYYZQYU.js +0 -18
  149. package/eventcatalog/src/components/EnvironmentDropdown.test.ts +0 -64
  150. package/eventcatalog/src/components/Grids/message-link.spec.ts +0 -59
  151. package/eventcatalog/src/components/MDX/CustomProperties/custom-properties.spec.ts +0 -38
  152. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +0 -116
  153. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +0 -114
  154. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +0 -91
  155. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +0 -33
  156. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +0 -84
  157. package/eventcatalog/src/components/Search/search-utils.spec.ts +0 -173
  158. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +0 -105
  159. package/eventcatalog/src/components/navigation-progress.spec.ts +0 -68
  160. package/eventcatalog/src/enterprise/directory/user-team-directory.spec.ts +0 -527
  161. package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +0 -302
  162. package/eventcatalog/src/enterprise/fields/fields-db.test.ts +0 -185
  163. package/eventcatalog/src/stores/eventcatalog-store.spec.ts +0 -60
  164. package/eventcatalog/src/utils/collections/glob-loader.spec.ts +0 -87
  165. package/dist/{chunk-6K7XZAYI.js → chunk-A2RZR3U4.js} +3 -3
@@ -1,15 +1,27 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import matter from 'gray-matter';
3
4
  import yaml from 'js-yaml';
4
5
  import { getResourceBasePath } from '@utils/resource-files';
5
6
 
7
+ /**
8
+ * A usage example for a message: any text file inside the message's `examples/` folder.
9
+ */
6
10
  export interface MessageExample {
11
+ /** File path relative to the `examples/` folder, for example `basic-order.json`. */
7
12
  fileName: string;
13
+ /** Display title: frontmatter `title`, else the first level-one heading, else the file name. */
8
14
  title: string;
15
+ /** File extension without the leading dot. */
9
16
  extension: string;
10
- content: string;
17
+ /** Markdown is rendered as prose; other formats and unsupported MDX are rendered as source. */
18
+ renderMode: 'markdown' | 'code';
19
+ /** Optional short description from frontmatter `summary`. */
11
20
  summary?: string;
21
+ /** Optional usage instructions from the legacy examples config. */
12
22
  usage?: string;
23
+ /** File contents. Markdown frontmatter is removed. */
24
+ content: string;
13
25
  }
14
26
 
15
27
  interface ExampleConfig {
@@ -18,68 +30,148 @@ interface ExampleConfig {
18
30
  usage?: string;
19
31
  }
20
32
 
21
- function loadConfig(examplesDir: string): Record<string, ExampleConfig> {
22
- const yamlPath = path.join(examplesDir, 'examples.config.yaml');
23
- const ymlPath = path.join(examplesDir, 'examples.config.yml');
24
- const jsonPath = path.join(examplesDir, 'examples.config.json');
25
-
26
- let configPath: string | null = null;
27
- if (fs.existsSync(yamlPath)) configPath = yamlPath;
28
- else if (fs.existsSync(ymlPath)) configPath = ymlPath;
29
- else if (fs.existsSync(jsonPath)) configPath = jsonPath;
33
+ const CONFIG_FILES = new Set(['examples.config.yaml', 'examples.config.yml', 'examples.config.json']);
34
+ const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
35
+ const CLIENT_RENDERED_MDX_COMPONENTS = new Set(['Column', 'Columns', 'CodeGroup']);
30
36
 
37
+ const loadConfig = (examplesDir: string): Record<string, ExampleConfig> => {
38
+ const configPath = ['examples.config.yaml', 'examples.config.yml', 'examples.config.json']
39
+ .map((fileName) => path.join(examplesDir, fileName))
40
+ .find(fs.existsSync);
31
41
  if (!configPath) return {};
32
42
 
33
43
  const raw = fs.readFileSync(configPath, 'utf-8');
34
- if (configPath.endsWith('.json')) {
35
- return JSON.parse(raw) || {};
36
- }
37
- return (yaml.load(raw) as Record<string, ExampleConfig>) || {};
38
- }
44
+ return configPath.endsWith('.json') ? JSON.parse(raw) || {} : (yaml.load(raw) as Record<string, ExampleConfig>) || {};
45
+ };
39
46
 
40
- const CONFIG_FILES = ['examples.config.yaml', 'examples.config.yml', 'examples.config.json'];
47
+ const humanizeFileName = (fileName: string) =>
48
+ path
49
+ .parse(fileName)
50
+ .name.replace(/[-_]+/g, ' ')
51
+ .replace(/\s+/g, ' ')
52
+ .trim()
53
+ .replace(/^\w/, (character) => character.toUpperCase());
41
54
 
42
- function collectFiles(dir: string, baseDir: string): Omit<MessageExample, 'summary' | 'usage'>[] {
43
- const results: Omit<MessageExample, 'summary' | 'usage'>[] = [];
55
+ const collectExampleFiles = (dir: string, baseDir: string): string[] => {
56
+ const results: string[] = [];
44
57
  const entries = fs.readdirSync(dir, { withFileTypes: true });
45
58
 
46
59
  for (const entry of entries) {
47
60
  const fullPath = path.join(dir, entry.name);
48
61
  if (entry.isDirectory()) {
49
- results.push(...collectFiles(fullPath, baseDir));
50
- } else if (entry.isFile() && !CONFIG_FILES.includes(entry.name)) {
51
- results.push({
52
- fileName: path.relative(baseDir, fullPath),
53
- title: path.parse(entry.name).name,
54
- extension: path.parse(entry.name).ext.slice(1),
55
- content: fs.readFileSync(fullPath, 'utf-8'),
56
- });
62
+ results.push(...collectExampleFiles(fullPath, baseDir));
63
+ } else if (entry.isFile() && !CONFIG_FILES.has(entry.name)) {
64
+ results.push(path.relative(baseDir, fullPath));
57
65
  }
58
66
  }
59
67
 
60
68
  return results;
69
+ };
70
+
71
+ const hasUnsupportedMdxComponent = (content: string) =>
72
+ [...content.matchAll(/<([A-Z][A-Za-z0-9.]*)\b/g)].some((match) => !CLIENT_RENDERED_MDX_COMPONENTS.has(match[1]));
73
+
74
+ /**
75
+ * Splits an example file into its title and Markdown body. The title comes from frontmatter,
76
+ * else the leading level-one heading, else the file name. The body is left as written.
77
+ */
78
+ export const parseExampleFile = (fileName: string, raw: string): MessageExample => {
79
+ const { data, content } = matter(raw);
80
+ const body = content.replace(/^\s*\n/, '');
81
+ let title = typeof data.title === 'string' && data.title.trim() ? data.title.trim() : undefined;
82
+ const extension = path.extname(fileName).slice(1).toLowerCase();
83
+
84
+ const headingMatch = body.match(/^#[ \t]+(.+)(?:\r?\n|$)/);
85
+ if (headingMatch) title = title ?? headingMatch[1].trim();
86
+
87
+ return {
88
+ fileName: fileName.replace(/\\/g, '/'),
89
+ title: title ?? humanizeFileName(fileName),
90
+ extension,
91
+ renderMode: extension === 'mdx' && hasUnsupportedMdxComponent(body) ? 'code' : 'markdown',
92
+ ...(typeof data.summary === 'string' && data.summary.trim() ? { summary: data.summary.trim() } : {}),
93
+ content: body.trim(),
94
+ };
95
+ };
96
+
97
+ const parseRawExampleFile = (fileName: string, raw: string, config: ExampleConfig = {}): MessageExample => ({
98
+ fileName: fileName.replace(/\\/g, '/'),
99
+ title: config.name?.trim() || humanizeFileName(fileName),
100
+ extension: path.extname(fileName).slice(1).toLowerCase(),
101
+ renderMode: 'code',
102
+ ...(config.summary?.trim() ? { summary: config.summary.trim() } : {}),
103
+ ...(config.usage?.trim() ? { usage: config.usage.trim() } : {}),
104
+ content: raw,
105
+ });
106
+
107
+ /** The parts of an `examples` collection entry the schema pages need. */
108
+ export interface ExampleEntryLike {
109
+ id: string;
110
+ filePath?: string;
111
+ body?: string;
112
+ data: { title?: string; summary?: string };
113
+ }
114
+
115
+ const normalize = (value: string) => path.resolve(value).replace(/\\/g, '/');
116
+
117
+ /**
118
+ * Selects the example entries that belong to a message: the files under the `examples/`
119
+ * folder beside the message file, sorted by path.
120
+ */
121
+ export function getExampleEntriesForMessage<T extends ExampleEntryLike>(entries: T[], messageFilePath?: string): T[] {
122
+ if (!messageFilePath) return [];
123
+ const examplesDir = `${normalize(path.dirname(messageFilePath))}/examples/`;
124
+
125
+ return entries
126
+ .filter((entry) => entry.filePath && normalize(entry.filePath).startsWith(examplesDir))
127
+ .sort((a, b) => (a.filePath as string).localeCompare(b.filePath as string));
61
128
  }
62
129
 
130
+ /**
131
+ * Display details for a collection entry, mirroring `parseExampleFile`: frontmatter title,
132
+ * else the first level-one heading, else the file name.
133
+ */
134
+ export function getExampleEntryDetails(entry: ExampleEntryLike, examplesDir?: string) {
135
+ const fileName =
136
+ entry.filePath && examplesDir
137
+ ? path.relative(examplesDir, entry.filePath).replace(/\\/g, '/')
138
+ : path.basename(entry.filePath ?? entry.id);
139
+ const parsed = parseExampleFile(fileName, entry.body ?? '');
140
+
141
+ return {
142
+ fileName,
143
+ title: entry.data.title?.trim() || parsed.title,
144
+ summary: entry.data.summary?.trim() || parsed.summary,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Reads usage examples stored in the `examples/` folder next to a message.
150
+ */
63
151
  export function getExamplesForResource(resource: { filePath?: string }): MessageExample[] {
64
152
  const basePath = getResourceBasePath(resource);
65
153
  if (!basePath) return [];
66
154
 
67
155
  const examplesDir = path.join(basePath, 'examples');
68
156
  if (!fs.existsSync(examplesDir)) return [];
69
-
70
157
  const config = loadConfig(examplesDir);
71
- const files = collectFiles(examplesDir, examplesDir);
72
-
73
- return files
74
- .map((file) => {
75
- const key = file.fileName.replace(/\\/g, '/');
76
- const meta = config[key] || {};
77
- return {
78
- ...file,
79
- title: meta.name || file.title,
80
- summary: meta.summary,
81
- usage: meta.usage,
82
- };
83
- })
84
- .sort((a, b) => a.fileName.localeCompare(b.fileName));
158
+
159
+ return collectExampleFiles(examplesDir, examplesDir)
160
+ .sort((a, b) => a.localeCompare(b))
161
+ .map((fileName) => {
162
+ const raw = fs.readFileSync(path.join(examplesDir, fileName), 'utf-8');
163
+ const normalizedFileName = fileName.replace(/\\/g, '/');
164
+ const extension = path.extname(fileName).toLowerCase();
165
+ if (MARKDOWN_EXTENSIONS.has(extension)) {
166
+ const example = parseExampleFile(fileName, raw);
167
+ const metadata = config[normalizedFileName];
168
+ return {
169
+ ...example,
170
+ ...(metadata?.name?.trim() ? { title: metadata.name.trim() } : {}),
171
+ ...(metadata?.summary?.trim() ? { summary: metadata.summary.trim() } : {}),
172
+ ...(metadata?.usage?.trim() ? { usage: metadata.usage.trim() } : {}),
173
+ };
174
+ }
175
+ return parseRawExampleFile(fileName, raw, config[normalizedFileName]);
176
+ });
85
177
  }
@@ -146,3 +146,46 @@ export const getFlowsNotInAnyResource = async (): Promise<Flow[]> => {
146
146
  });
147
147
  return flowsNotInAnyResource;
148
148
  };
149
+
150
+ /** A flow that includes a message in one of its steps, reduced to what the schema pages display. */
151
+ export interface FlowReference {
152
+ id: string;
153
+ version: string;
154
+ name?: string;
155
+ summary?: string;
156
+ }
157
+
158
+ /**
159
+ * Finds the flows whose steps send or receive each of the given messages. Step pointers are
160
+ * matched the same way flow diagrams resolve them (exact version, then semver range, then
161
+ * latest). Returns a map keyed by `${collection}:${id}:${version}` of the resolved message.
162
+ */
163
+ export const getFlowsForMessages = async (
164
+ messages: { collection: string; data: { id: string; version: string } }[]
165
+ ): Promise<Map<string, FlowReference[]>> => {
166
+ const flows = await getCollection('flows');
167
+ const messageMap = createVersionedMap(messages);
168
+ const flowsByMessage = new Map<string, FlowReference[]>();
169
+
170
+ for (const flow of flows) {
171
+ if (flow.data.hidden === true) continue;
172
+ const reference: FlowReference = {
173
+ id: flow.data.id,
174
+ version: flow.data.version,
175
+ ...(flow.data.name ? { name: flow.data.name } : {}),
176
+ ...(flow.data.summary ? { summary: flow.data.summary } : {}),
177
+ };
178
+ const seen = new Set<string>();
179
+ for (const step of flow.data.steps ?? []) {
180
+ if (!step.message) continue;
181
+ const message = findInMap(messageMap, step.message.id, step.message.version);
182
+ if (!message) continue;
183
+ const key = `${message.collection}:${message.data.id}:${message.data.version}`;
184
+ if (seen.has(key)) continue;
185
+ seen.add(key);
186
+ flowsByMessage.set(key, [...(flowsByMessage.get(key) ?? []), reference]);
187
+ }
188
+ }
189
+
190
+ return flowsByMessage;
191
+ };
@@ -49,7 +49,7 @@ export const hydrateProducersAndConsumers = ({
49
49
  const toResult = <T extends CollectionEntry<'agents'> | CollectionEntry<'services'> | CollectionEntry<'data-products'>>(
50
50
  resource: T
51
51
  ) => {
52
- if (!hydrate) return { id: resource.data.id, version: resource.data.version };
52
+ if (!hydrate) return { id: resource.data.id, version: resource.data.version, collection: resource.collection };
53
53
  return resource;
54
54
  };
55
55
 
@@ -193,7 +193,7 @@ export const lookupProducersAndConsumers = ({ message, index, hydrate = true }:
193
193
  const toResult = <T extends CollectionEntry<'agents'> | CollectionEntry<'services'> | CollectionEntry<'data-products'>>(
194
194
  resource: T
195
195
  ) => {
196
- if (!hydrate) return { id: resource.data.id, version: resource.data.version };
196
+ if (!hydrate) return { id: resource.data.id, version: resource.data.version, collection: resource.collection };
197
197
  return resource;
198
198
  };
199
199
 
@@ -39,6 +39,21 @@ export const getSchemaURL = (resource: CollectionEntry<PageTypes> | any) => {
39
39
  return path.join(publicPath, schemaFilePath ?? '');
40
40
  };
41
41
 
42
+ /** File extension EventCatalog uses for a schema format, for schemas that have no file name. */
43
+ export const getSchemaExtensionForFormat = (format?: string) => {
44
+ switch (format?.toLowerCase()) {
45
+ case 'jsonschema':
46
+ case 'json':
47
+ return 'json';
48
+ case 'avro':
49
+ return 'avsc';
50
+ case 'protobuf':
51
+ return 'proto';
52
+ default:
53
+ return format ?? '';
54
+ }
55
+ };
56
+
42
57
  export const getSchemaFormatFromURL = (url: string) => {
43
58
  const pathParts = url.split('.');
44
59
  const format = pathParts[pathParts.length - 1];
@@ -515,19 +515,21 @@ const getNodesAndEdges = async ({
515
515
  (receive) => receive.id === message.data.id && versionMatches(receive.version, message.data.version)
516
516
  )?.from ?? [];
517
517
 
518
- for (const producerChannel of producerChannels) {
519
- const producerChannelValue = findInMap(
520
- channelMap,
521
- producerChannel.id,
522
- producerChannel.version
523
- ) as CollectionEntry<'channels'>;
524
-
525
- for (const consumerChannel of consumerChannels) {
526
- const consumerChannelValue = findInMap(
527
- channelMap,
528
- consumerChannel.id,
529
- consumerChannel.version
530
- ) as CollectionEntry<'channels'>;
518
+ const resolvedProducerChannels = producerChannels
519
+ .map(
520
+ (producerChannel) =>
521
+ findInMap(channelMap, producerChannel.id, producerChannel.version) as CollectionEntry<'channels'> | undefined
522
+ )
523
+ .filter((resolved): resolved is CollectionEntry<'channels'> => resolved !== undefined);
524
+ const resolvedConsumerChannels = consumerChannels
525
+ .map(
526
+ (consumerChannel) =>
527
+ findInMap(channelMap, consumerChannel.id, consumerChannel.version) as CollectionEntry<'channels'> | undefined
528
+ )
529
+ .filter((resolved): resolved is CollectionEntry<'channels'> => resolved !== undefined);
530
+
531
+ for (const producerChannelValue of resolvedProducerChannels) {
532
+ for (const consumerChannelValue of resolvedConsumerChannels) {
531
533
  const channelChainToRender = getChannelChain(producerChannelValue, consumerChannelValue, channels);
532
534
 
533
535
  // If there is a chain between them we need to render them al
@@ -573,8 +575,8 @@ const getNodesAndEdges = async ({
573
575
  }
574
576
  }
575
577
 
576
- // If producer does not have a any channels defined, we need to connect the message to the consumer directly
577
- if (producerChannels.length === 0 && channel) {
578
+ // If producer does not have any resolved channels, connect via the consumer channel
579
+ if (resolvedProducerChannels.length === 0 && channel) {
578
580
  // Create the channel node
579
581
  nodes.push(
580
582
  createNode({
@@ -1,15 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
- import type { SchemaItem, SchemaDetails, Producer, Consumer } from '@components/SchemaExplorer/types';
2
+ import type { SchemaItem, SchemaDetails, Producer, Consumer, MessageChannel } from '@components/SchemaExplorer/types';
3
3
  import { getEvents } from '@utils/collections/events';
4
4
  import { getCommands } from '@utils/collections/commands';
5
5
  import { getQueries } from '@utils/collections/queries';
6
6
  import { getServices, getSpecificationsForService } from '@utils/collections/services';
7
7
  import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
8
8
  import { getDataProducts } from '@utils/collections/data-products';
9
+ import { getAgents } from '@utils/collections/agents';
10
+ import { getFlowsForMessages } from '@utils/collections/flows';
9
11
  import { getOwner } from '@utils/collections/owners';
10
12
  import { buildUrl } from '@utils/url-builder';
13
+ import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
11
14
  import { resourceFileExists, readResourceFile } from '@utils/resource-files';
12
15
  import { getExamplesForResource } from '@utils/collections/examples';
16
+ import { attachFlowGraphs, getMessageUsageGraph } from '@utils/schema-usage-graph';
13
17
  import { getCollection } from 'astro:content';
14
18
  import path from 'path';
15
19
 
@@ -37,12 +41,51 @@ async function buildRegistry() {
37
41
 
38
42
  // Fetch all services
39
43
  const services = await getServices({ getAllVersions: true });
44
+ // Producers and consumers can be services, agents, or data products. Index all three so
45
+ // compact { id, version } references can be resolved to a resource for display.
46
+ const [agents, dataProductsForRelationships] = await Promise.all([
47
+ getAgents({ getAllVersions: true }),
48
+ getDataProducts({ getAllVersions: true }),
49
+ ]);
50
+ type RelationshipResource = (typeof services)[number] | (typeof agents)[number] | (typeof dataProductsForRelationships)[number];
51
+ const relationshipResources: RelationshipResource[] = [...services, ...agents, ...dataProductsForRelationships];
52
+ const resourcesByReference = new Map<string, RelationshipResource>();
53
+ const latestResourceByCollectionAndId = new Map<string, RelationshipResource>();
54
+ for (const resource of relationshipResources) {
55
+ const collectionAndId = `${resource.collection}:${resource.data.id}`;
56
+ resourcesByReference.set(`${collectionAndId}:${resource.data.version}`, resource);
57
+ if (!latestResourceByCollectionAndId.has(collectionAndId)) latestResourceByCollectionAndId.set(collectionAndId, resource);
58
+ }
59
+ const toSchemaRelationships = (references: unknown): Producer[] =>
60
+ ((references as { id: string; version: string; collection?: Producer['collection'] }[] | undefined) ?? []).map(
61
+ (reference) => {
62
+ const collection = reference.collection ?? 'services';
63
+ const collectionAndId = `${collection}:${reference.id}`;
64
+ const resource =
65
+ resourcesByReference.get(`${collectionAndId}:${reference.version}`) ??
66
+ latestResourceByCollectionAndId.get(collectionAndId);
67
+ return {
68
+ id: reference.id,
69
+ version: reference.version,
70
+ collection,
71
+ ...(resource?.data.name ? { name: resource.data.name } : {}),
72
+ ...(resource?.data.summary ? { summary: resource.data.summary } : {}),
73
+ };
74
+ }
75
+ );
76
+ const toMessageChannels = (channels: unknown): MessageChannel[] =>
77
+ ((channels as { data: { id: string; version: string; name?: string } }[] | undefined) ?? []).map((channel) => ({
78
+ id: channel.data.id,
79
+ version: channel.data.version,
80
+ ...(channel.data.name ? { name: channel.data.name } : {}),
81
+ }));
40
82
 
41
83
  // Combine all messages
42
84
  const allMessages = [...events, ...commands, ...queries];
43
85
  const messagesBySchemaReference = new Map(
44
86
  allMessages.map((message) => [`${message.collection}:${message.data.id}:${message.data.version}`, message])
45
87
  );
88
+ const flowsByMessage = await getFlowsForMessages(allMessages);
46
89
 
47
90
  // Read message schemas from the generated schemas collection.
48
91
  const messagesWithSchemas = await Promise.all(
@@ -51,11 +94,16 @@ async function buildRegistry() {
51
94
  `${schema.data.message.collectionName}:${schema.data.message.id}:${schema.data.message.version}`
52
95
  );
53
96
  const schemaPath = schema.data.file || schema.data.source.path || '';
54
- const schemaExtension = path.extname(schemaPath).slice(1) || schema.data.format;
97
+ const schemaExtension = path.extname(schemaPath).slice(1) || getSchemaExtensionForFormat(schema.data.format);
55
98
  // The collection types describe raw content references. With
56
- // hydrateServices: false, the loaders return compact { id, version } pairs.
57
- const producers = (message?.data.producers || []) as unknown as Producer[];
58
- const consumers = (message?.data.consumers || []) as unknown as Consumer[];
99
+ // hydrateServices: false, the loaders return compact { id, version } pairs,
100
+ // so look the service name and summary up for display.
101
+ const producers = toSchemaRelationships(message?.data.producers);
102
+ const consumers = toSchemaRelationships(message?.data.consumers);
103
+ const channels = toMessageChannels(message?.data.messageChannels);
104
+ const flows =
105
+ flowsByMessage.get(`${schema.data.message.collectionName}:${schema.data.message.id}:${schema.data.message.version}`) ??
106
+ [];
59
107
 
60
108
  return {
61
109
  collection: schema.data.message.collectionName,
@@ -70,22 +118,40 @@ async function buildRegistry() {
70
118
  // regardless of how many resources reference the message.
71
119
  producerName: producers[0]?.id,
72
120
  },
73
- loadDetails: () => {
121
+ loadDetails: async () => {
74
122
  let examples: SchemaDetails['examples'] = [];
123
+ let graph: SchemaDetails['graph'];
75
124
  if (message) {
76
125
  try {
77
126
  examples = getExamplesForResource(message);
78
127
  } catch (error) {
79
128
  console.error(`Error reading examples for ${message.data.id}:`, error);
80
129
  }
130
+ try {
131
+ graph = await getMessageUsageGraph({
132
+ collection: message.collection,
133
+ id: message.data.id,
134
+ version: message.data.version,
135
+ });
136
+ } catch (error) {
137
+ console.error(`Error building usage graph for ${message.data.id}:`, error);
138
+ }
81
139
  }
82
140
  return {
83
141
  schemaContent: schema.data.content || '',
84
142
  examples,
85
- data: { producers, consumers },
143
+ ...(graph ? { graph } : {}),
144
+ data: {
145
+ producers,
146
+ consumers,
147
+ flows: await attachFlowGraphs(flows, { id: schema.data.message.id, version: schema.data.message.version }),
148
+ channels,
149
+ },
86
150
  };
87
151
  },
88
152
  schemaExtension,
153
+ ...(schema.data.name ? { schemaName: schema.data.name } : {}),
154
+ ...(schema.data.ref ? { schemaRef: schema.data.ref, source: schema.data.source } : {}),
89
155
  };
90
156
  })
91
157
  );
@@ -0,0 +1,92 @@
1
+ import type { Edge, Node } from '@xyflow/react';
2
+ import config from '@config';
3
+ import {
4
+ getNodesAndEdgesForCommands,
5
+ getNodesAndEdgesForEvents,
6
+ getNodesAndEdgesForQueries,
7
+ } from '@utils/node-graphs/message-node-graph';
8
+ import { getNodesAndEdges as getNodesAndEdgesForFlow } from '@utils/node-graphs/flows-node-graph';
9
+ import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
10
+
11
+ export interface MessageUsageGraph {
12
+ nodes: Node[];
13
+ edges: Edge[];
14
+ }
15
+
16
+ const graphBuilders = {
17
+ events: getNodesAndEdgesForEvents,
18
+ commands: getNodesAndEdgesForCommands,
19
+ queries: getNodesAndEdgesForQueries,
20
+ } as const;
21
+
22
+ /**
23
+ * The same producer → message → consumer graph the message docs page renders, reduced to the
24
+ * fields the visualiser needs so it can be sent to the schema pages.
25
+ */
26
+ export const getMessageUsageGraph = async ({
27
+ collection,
28
+ id,
29
+ version,
30
+ }: {
31
+ collection: string;
32
+ id: string;
33
+ version: string;
34
+ }): Promise<MessageUsageGraph | undefined> => {
35
+ const build = graphBuilders[collection as keyof typeof graphBuilders];
36
+ if (!build) return undefined;
37
+ const { nodes, edges } = await build({
38
+ id,
39
+ version,
40
+ mode: 'simple',
41
+ channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat',
42
+ });
43
+ return compactVisualiserGraph(nodes as Node[], edges as Edge[]);
44
+ };
45
+
46
+ /** Identifies the message a diagram is being viewed from, so its node can be highlighted. */
47
+ export interface FocusedMessage {
48
+ id: string;
49
+ version: string;
50
+ }
51
+
52
+ /**
53
+ * The flow diagram the flow docs page renders, compacted for the schema pages. Steps that
54
+ * send or receive the focused message get the visualiser's "Viewing" indicator.
55
+ */
56
+ export const getFlowGraph = async ({
57
+ id,
58
+ version,
59
+ focus,
60
+ }: {
61
+ id: string;
62
+ version: string;
63
+ focus?: FocusedMessage;
64
+ }): Promise<MessageUsageGraph> => {
65
+ const { nodes, edges } = await getNodesAndEdgesForFlow({ id, version, mode: 'simple' });
66
+ const focused = (nodes as Node[]).map((node) => {
67
+ const message = node.data?.message as { id?: string; version?: string } | undefined;
68
+ return focus && message?.id === focus.id && message?.version === focus.version
69
+ ? { ...node, data: { ...node.data, isFocused: true } }
70
+ : node;
71
+ });
72
+ return compactVisualiserGraph(focused, edges as Edge[]);
73
+ };
74
+
75
+ /**
76
+ * Adds each flow's diagram to the flow usages, highlighting the focused message's steps.
77
+ * A flow whose diagram fails to build is kept without one.
78
+ */
79
+ export const attachFlowGraphs = <T extends { id: string; version: string }>(
80
+ flows: T[],
81
+ focus?: FocusedMessage
82
+ ): Promise<(T & { graph?: MessageUsageGraph })[]> =>
83
+ Promise.all(
84
+ flows.map(async (flow) => {
85
+ try {
86
+ return { ...flow, graph: await getFlowGraph({ ...flow, focus }) };
87
+ } catch (error) {
88
+ console.error(`Error building flow graph for ${flow.id}:`, error);
89
+ return flow;
90
+ }
91
+ })
92
+ );
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "4.10.15",
10
+ "version": "4.11.1",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -27,6 +27,7 @@
27
27
  "!eventcatalog/dist/",
28
28
  "!eventcatalog/node_modules/",
29
29
  "!eventcatalog/**/__tests__/",
30
+ "!eventcatalog/**/*.{spec,test}.{js,jsx,cjs,mjs,ts,tsx,cts,mts}",
30
31
  "bin/",
31
32
  "dist/"
32
33
  ],
@@ -119,9 +120,9 @@
119
120
  "update-notifier": "^7.3.1",
120
121
  "uuid": "^11.1.1",
121
122
  "zod": "^4.3.6",
122
- "@eventcatalog/linter": "1.1.19",
123
- "@eventcatalog/sdk": "2.29.0",
124
- "@eventcatalog/visualiser": "^4.1.4"
123
+ "@eventcatalog/linter": "1.1.20",
124
+ "@eventcatalog/visualiser": "^4.1.4",
125
+ "@eventcatalog/sdk": "2.29.1"
125
126
  },
126
127
  "devDependencies": {
127
128
  "@astrojs/check": "^0.9.10",
@@ -1,18 +0,0 @@
1
- // src/astro-output.ts
2
- var createAstroLineFilter = () => {
3
- return (line) => {
4
- const isIgnoredGetStaticPathsWarning = line.includes("[router]") && line.includes("getStaticPaths() ignored in dynamic page");
5
- return line.includes("[glob-loader]") || isIgnoredGetStaticPathsWarning || /^\s*The collection ".*" does not exist or is empty\. Please check your content config file for errors\.\s*$/.test(line);
6
- };
7
- };
8
- var createAstroDevLineFilter = () => {
9
- const shouldFilterAstroLine = createAstroLineFilter();
10
- return (line) => {
11
- return shouldFilterAstroLine(line) || line.includes("[router]");
12
- };
13
- };
14
-
15
- export {
16
- createAstroLineFilter,
17
- createAstroDevLineFilter
18
- };