@eventcatalog/core 4.10.8 → 4.10.10

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 (44) 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-W6IMDOJJ.js → chunk-K45ABG6N.js} +1 -1
  6. package/dist/{chunk-LB5EMDAY.js → chunk-PR77FB54.js} +1 -1
  7. package/dist/{chunk-CDWE6WWB.js → chunk-QXANW76J.js} +1 -1
  8. package/dist/{chunk-W5C4K7F2.js → chunk-U6R6BFJ5.js} +1 -1
  9. package/dist/{chunk-DRK5QSQC.js → chunk-Y6VX2CGL.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +6 -2
  19. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +58 -12
  20. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +5 -5
  21. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +2 -0
  22. package/eventcatalog/src/components/SchemaExplorer/types.ts +10 -1
  23. package/eventcatalog/src/components/SchemaExplorer/useSchemaDetails.ts +69 -0
  24. package/eventcatalog/src/components/SchemaExplorer/utils.ts +12 -0
  25. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.spec.ts +52 -0
  26. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +20 -11
  27. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +3 -269
  28. package/eventcatalog/src/pages/schemas/explorer/content/[key].json.ts +17 -0
  29. package/eventcatalog/src/stores/sidebar-store/state.ts +3 -28
  30. package/eventcatalog/src/utils/collections/field-usage.ts +48 -0
  31. package/eventcatalog/src/utils/node-graphs/channel-node-graph.ts +2 -2
  32. package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +246 -0
  33. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -2
  34. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +2 -2
  35. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +2 -2
  36. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +2 -2
  37. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +2 -2
  38. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +2 -2
  39. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +2 -2
  40. package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +2 -2
  41. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +8 -3
  42. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +30 -2
  43. package/eventcatalog/src/utils/schema-explorer.ts +276 -0
  44. 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 = "4.10.8";
39
+ var version = "4.10.10";
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-CDWE6WWB.js";
4
- import "../chunk-LB5EMDAY.js";
3
+ } from "../chunk-QXANW76J.js";
4
+ import "../chunk-PR77FB54.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 = "4.10.8";
143
+ var version = "4.10.10";
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-W6IMDOJJ.js";
4
- import "../chunk-CDWE6WWB.js";
3
+ } from "../chunk-K45ABG6N.js";
4
+ import "../chunk-QXANW76J.js";
5
5
  import "../chunk-ZAZHAVKB.js";
6
- import "../chunk-LB5EMDAY.js";
6
+ import "../chunk-PR77FB54.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-CDWE6WWB.js";
3
+ } from "./chunk-QXANW76J.js";
4
4
  import {
5
5
  countResources,
6
6
  hashCatalogContent,
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "4.10.8";
2
+ var version = "4.10.10";
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-LB5EMDAY.js";
3
+ } from "./chunk-PR77FB54.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-LB5EMDAY.js";
3
+ } from "./chunk-PR77FB54.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-W5C4K7F2.js";
3
+ } from "./chunk-U6R6BFJ5.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 = "4.10.8";
28
+ var version = "4.10.10";
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-LB5EMDAY.js";
3
+ } from "./chunk-PR77FB54.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 = "4.10.8";
147
+ var version = "4.10.10";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -4,8 +4,8 @@ import {
4
4
  import "./chunk-CA4U2JP7.js";
5
5
  import {
6
6
  log_build_default
7
- } from "./chunk-W6IMDOJJ.js";
8
- import "./chunk-CDWE6WWB.js";
7
+ } from "./chunk-K45ABG6N.js";
8
+ import "./chunk-QXANW76J.js";
9
9
  import "./chunk-ZAZHAVKB.js";
10
10
  import {
11
11
  FederationConflictError,
@@ -31,10 +31,10 @@ import {
31
31
  } from "./chunk-B7HCX5HM.js";
32
32
  import {
33
33
  generate
34
- } from "./chunk-DRK5QSQC.js";
34
+ } from "./chunk-Y6VX2CGL.js";
35
35
  import {
36
36
  logger
37
- } from "./chunk-W5C4K7F2.js";
37
+ } from "./chunk-U6R6BFJ5.js";
38
38
  import {
39
39
  resolve_catalog_dependencies_default
40
40
  } from "./chunk-LHR4G2UO.js";
@@ -54,7 +54,7 @@ import {
54
54
  import "./chunk-W5JQON7Z.js";
55
55
  import {
56
56
  VERSION
57
- } from "./chunk-LB5EMDAY.js";
57
+ } from "./chunk-PR77FB54.js";
58
58
  import {
59
59
  linkCoreNodeModules,
60
60
  resolveInstalledCoreNodeModules
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 = "4.10.8";
111
+ var version = "4.10.10";
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-DRK5QSQC.js";
4
- import "./chunk-W5C4K7F2.js";
5
- import "./chunk-LB5EMDAY.js";
3
+ } from "./chunk-Y6VX2CGL.js";
4
+ import "./chunk-U6R6BFJ5.js";
5
+ import "./chunk-PR77FB54.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 = "4.10.8";
39
+ var version = "4.10.10";
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-W5C4K7F2.js";
4
- import "../chunk-LB5EMDAY.js";
3
+ } from "../chunk-U6R6BFJ5.js";
4
+ import "../chunk-PR77FB54.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -31,6 +31,7 @@ import { getNodesAndEdges as getNodesAndEdgesForChannel } from '@utils/node-grap
31
31
  import config from '@config';
32
32
  import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
33
33
  import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
34
+ import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
34
35
 
35
36
  const isChatEnabled = isEventCatalogChatEnabled();
36
37
 
@@ -198,13 +199,16 @@ if (collection === 'services-containers') {
198
199
  const resourceKey = buildResourceKey(collection, id, version);
199
200
  const savedLayout = await loadSavedLayout(resourceKey);
200
201
  const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
202
+ // Drop hydrated collection entries / markdown bodies before Astro serialises
203
+ // the graph into the page. Visualiser components only need a small field set.
204
+ const { nodes: clientNodes, edges: clientEdges } = compactVisualiserGraph(nodesWithLayout as any[], edges as any[]);
201
205
  ---
202
206
 
203
207
  <div>
204
208
  <AstroNodeGraph
205
209
  id={id}
206
- nodes={nodesWithLayout}
207
- edges={edges}
210
+ nodes={clientNodes}
211
+ edges={clientEdges}
208
212
  title={title}
209
213
  hrefLabel={href?.label}
210
214
  href={href?.url}
@@ -24,7 +24,8 @@ import ExamplesViewer from './ExamplesViewer';
24
24
  import VersionHistoryModal from './VersionHistoryModal';
25
25
  import SchemaCodeModal from './SchemaCodeModal';
26
26
  import SchemaViewerModal from './SchemaViewerModal';
27
- import { copyToClipboard, downloadSchema, getSchemaTypeLabel, ICON_SPECS, extractServiceName } from './utils';
27
+ import { copyToClipboard, downloadSchema, getSchemaTypeLabel, ICON_SPECS, getSchemaRelationshipReference } from './utils';
28
+ import { createSchemaDetailsLoader, useSchemaDetails } from './useSchemaDetails';
28
29
  import { parseProtobufSchema } from '@utils/protobuf-schema';
29
30
  import type { SchemaItem, VersionDiff, Owner, Producer, Consumer } from './types';
30
31
 
@@ -39,7 +40,7 @@ interface SchemaDetailsPanelProps {
39
40
  }
40
41
 
41
42
  export default function SchemaDetailsPanel({
42
- message,
43
+ message: metadataMessage,
43
44
  availableVersions,
44
45
  selectedVersion,
45
46
  onVersionChange,
@@ -47,6 +48,9 @@ export default function SchemaDetailsPanel({
47
48
  showOwners = true,
48
49
  showProducersConsumers = true,
49
50
  }: SchemaDetailsPanelProps) {
51
+ const [loadDetails] = useState(createSchemaDetailsLoader);
52
+ const content = useSchemaDetails(metadataMessage, loadDetails);
53
+ const message = content.message!;
50
54
  const [copiedId, setCopiedId] = useState<string | null>(null);
51
55
  const [activeTab, setActiveTab] = useState<'code' | 'schema' | 'diff' | 'api' | 'examples'>('code');
52
56
  const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
@@ -88,18 +92,23 @@ export default function SchemaDetailsPanel({
88
92
  setIsDiffModalOpen(false);
89
93
  }, [schemaResourceKey, defaultFromVersion, defaultToVersion]);
90
94
 
91
- const diffFromItem = useMemo(
95
+ const diffFromMetadata = useMemo(
92
96
  () => uniqueAvailableVersions.find((version) => version.data.version === diffFromVersion),
93
97
  [diffFromVersion, uniqueAvailableVersions]
94
98
  );
95
- const diffToItem = useMemo(
99
+ const diffToMetadata = useMemo(
96
100
  () => uniqueAvailableVersions.find((version) => version.data.version === diffToVersion),
97
101
  [diffToVersion, uniqueAvailableVersions]
98
102
  );
103
+ const comparing = activeTab === 'diff' || isDiffModalOpen;
104
+ const fromContent = useSchemaDetails(diffFromMetadata, loadDetails, comparing);
105
+ const toContent = useSchemaDetails(diffToMetadata, loadDetails, comparing);
106
+ const diffFromItem = fromContent.message;
107
+ const diffToItem = toContent.message;
99
108
  const hasDiffFromContent = !!diffFromItem?.schemaContent?.trim();
100
109
  const hasDiffToContent = !!diffToItem?.schemaContent?.trim();
101
110
  const selectedDiff: VersionDiff | null = useMemo(() => {
102
- if (!diffFromItem || !diffToItem) return null;
111
+ if (!comparing || !diffFromItem || !diffToItem) return null;
103
112
  if (diffFromItem.data.version === diffToItem.data.version) return null;
104
113
  if (!diffFromItem.schemaContent?.trim() || !diffToItem.schemaContent?.trim()) return null;
105
114
 
@@ -126,7 +135,7 @@ export default function SchemaDetailsPanel({
126
135
  fromContent: diffFromItem.schemaContent,
127
136
  toContent: diffToItem.schemaContent,
128
137
  };
129
- }, [diffFromItem, diffToItem]);
138
+ }, [comparing, diffFromItem, diffToItem]);
130
139
  const selectedDiffs = selectedDiff ? [selectedDiff] : [];
131
140
 
132
141
  // Check if this is a JSON schema
@@ -359,7 +368,9 @@ export default function SchemaDetailsPanel({
359
368
 
360
369
  {/* Tab content */}
361
370
  <div className="flex-1 min-h-0 overflow-hidden p-6">
362
- {activeTab === 'examples' && examples.length > 0 ? (
371
+ {content.loading || content.error ? (
372
+ <SchemaLoadingState loading={content.loading} error={content.error} retry={content.retry} />
373
+ ) : activeTab === 'examples' && examples.length > 0 ? (
363
374
  <ExamplesViewer examples={examples} />
364
375
  ) : activeTab === 'api' ? (
365
376
  <ApiContentViewer
@@ -416,7 +427,16 @@ export default function SchemaDetailsPanel({
416
427
  </button>
417
428
  </div>
418
429
  <div className="min-h-0 flex-1 overflow-hidden">
419
- {diffFromVersion === diffToVersion ? (
430
+ {fromContent.loading || toContent.loading || fromContent.error || toContent.error ? (
431
+ <SchemaLoadingState
432
+ loading={fromContent.loading || toContent.loading}
433
+ error={fromContent.error || toContent.error}
434
+ retry={() => {
435
+ fromContent.retry();
436
+ toContent.retry();
437
+ }}
438
+ />
439
+ ) : diffFromVersion === diffToVersion ? (
420
440
  <div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
421
441
  <p className="text-sm">Select two different versions</p>
422
442
  </div>
@@ -525,11 +545,11 @@ export default function SchemaDetailsPanel({
525
545
  </div>
526
546
  <div className="space-y-1">
527
547
  {producers.map((producer: Producer, idx: number) => {
528
- const serviceName = extractServiceName(producer.id);
548
+ const { id: serviceName, version } = getSchemaRelationshipReference(producer);
529
549
  return (
530
550
  <a
531
551
  key={`${producer.id}-${idx}`}
532
- href={buildUrl(`/docs/services/${serviceName}/${producer.version}`)}
552
+ href={buildUrl(`/docs/services/${serviceName}/${version}`)}
533
553
  className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
534
554
  >
535
555
  <ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
@@ -551,11 +571,11 @@ export default function SchemaDetailsPanel({
551
571
  </div>
552
572
  <div className="space-y-1">
553
573
  {consumers.map((consumer: Consumer, idx: number) => {
554
- const serviceName = extractServiceName(consumer.id);
574
+ const { id: serviceName, version } = getSchemaRelationshipReference(consumer);
555
575
  return (
556
576
  <a
557
577
  key={`${consumer.id}-${idx}`}
558
- href={buildUrl(`/docs/services/${serviceName}/${consumer.version}`)}
578
+ href={buildUrl(`/docs/services/${serviceName}/${version}`)}
559
579
  className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
560
580
  >
561
581
  <ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
@@ -615,3 +635,29 @@ export default function SchemaDetailsPanel({
615
635
  </div>
616
636
  );
617
637
  }
638
+
639
+ function SchemaLoadingState({ loading, error, retry }: { loading: boolean; error?: string; retry: () => void }) {
640
+ if (!error) {
641
+ if (!loading) return null;
642
+ return (
643
+ <div role="status" className="flex h-full items-center justify-center gap-2 text-sm text-[rgb(var(--ec-page-text-muted))]">
644
+ <span
645
+ aria-hidden="true"
646
+ className="h-4 w-4 rounded-full border-2 border-[rgb(var(--ec-page-border))] border-t-[rgb(var(--ec-page-text-muted))] motion-safe:animate-spin"
647
+ />
648
+ <span>Loading schema…</span>
649
+ </div>
650
+ );
651
+ }
652
+
653
+ return (
654
+ <div className="flex h-full flex-col items-center justify-center gap-3 text-[rgb(var(--ec-page-text-muted))]" role="alert">
655
+ <p>{error}</p>
656
+ {error && (
657
+ <button type="button" onClick={retry} className="text-[rgb(var(--ec-accent))] hover:underline">
658
+ Try again
659
+ </button>
660
+ )}
661
+ </div>
662
+ );
663
+ }
@@ -36,10 +36,10 @@ function getSpecFile(item: SchemaItem): string {
36
36
  * multiple specs of the same type (e.g. two OpenAPI specs).
37
37
  */
38
38
  function getGroupKey(item: SchemaItem): string {
39
- if (item.collection === 'services') {
40
- return `${item.data.id}__${item.specType || 'unknown'}__${getSpecFile(item)}`;
39
+ if (item.collection === 'services' || item.collection === 'domains') {
40
+ return `${item.collection}__${item.data.id}__${item.specType || 'unknown'}__${getSpecFile(item)}`;
41
41
  }
42
- return item.data.id;
42
+ return `${item.collection}__${item.data.id}`;
43
43
  }
44
44
 
45
45
  interface SchemaExplorerProps {
@@ -110,7 +110,7 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
110
110
  params.set('version', message.data.version);
111
111
  params.set('collection', message.collection);
112
112
 
113
- if (message.collection === 'services') {
113
+ if (message.collection === 'services' || message.collection === 'domains') {
114
114
  params.set('specType', message.specType || 'unknown');
115
115
  const specFile = getSpecFile(message);
116
116
  if (specFile) {
@@ -242,7 +242,7 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
242
242
  const versionMatch = msg.data.version === version;
243
243
  const collectionMatch = !collection || msg.collection === collection;
244
244
 
245
- if (msg.collection === 'services') {
245
+ if (msg.collection === 'services' || msg.collection === 'domains') {
246
246
  const specTypeMatch = !specType || msg.specType === specType;
247
247
  const msgSpecFile = getSpecFile(msg);
248
248
  const specFilenameMatch = !specFilename || msgSpecFile === specFilename;
@@ -11,6 +11,8 @@ interface SchemaListItemProps {
11
11
  }
12
12
 
13
13
  function getNamespace(message: SchemaItem): string | null {
14
+ if (message.data.producerName) return message.data.producerName;
15
+
14
16
  const producers = message.data.producers || [];
15
17
  const firstProducer = producers[0];
16
18
 
@@ -20,7 +20,7 @@ export interface Owner {
20
20
  }
21
21
 
22
22
  export interface SchemaItem {
23
- collection: CollectionMessageTypes | 'services' | 'data-products';
23
+ collection: CollectionMessageTypes | 'services' | 'domains' | 'data-products';
24
24
  data: {
25
25
  id: string;
26
26
  name: string;
@@ -29,9 +29,12 @@ export interface SchemaItem {
29
29
  schemaPath?: string;
30
30
  producers?: Producer[];
31
31
  consumers?: Consumer[];
32
+ producerName?: string;
32
33
  owners?: Owner[];
33
34
  };
34
35
  schemaContent?: string;
36
+ /** Internal URL for loading this version's content without embedding it in the page. */
37
+ contentUrl?: string;
35
38
  schemaExtension?: string;
36
39
  specType?: string;
37
40
  specName?: string;
@@ -44,6 +47,12 @@ export interface SchemaItem {
44
47
  examples?: MessageExample[];
45
48
  }
46
49
 
50
+ export interface SchemaDetails {
51
+ schemaContent: string;
52
+ examples: MessageExample[];
53
+ data?: Pick<SchemaItem['data'], 'producers' | 'consumers'>;
54
+ }
55
+
47
56
  export interface VersionDiff {
48
57
  fromVersion: string;
49
58
  toVersion: string;
@@ -0,0 +1,69 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { SchemaDetails, SchemaItem } from './types';
3
+
4
+ // Scoped to the explorer component's lifetime by the caller. Also deduplicates
5
+ // requests when the selected version is one of the comparison versions.
6
+ export const createSchemaDetailsLoader = () => {
7
+ const requests = new Map<string, Promise<SchemaDetails>>();
8
+ const resolved = new Map<string, SchemaDetails>();
9
+ const load = (url: string): Promise<SchemaDetails> => {
10
+ const existing = requests.get(url);
11
+ if (existing) return existing;
12
+ const request = fetch(url)
13
+ .then(async (response) => {
14
+ if (!response.ok) throw new Error('Unable to load schema content. Please try again.');
15
+ const details = await response.json();
16
+ if (typeof details.schemaContent !== 'string' || !Array.isArray(details.examples)) {
17
+ throw new Error('Unable to load schema content. Please try again.');
18
+ }
19
+ resolved.set(url, details);
20
+ return details as SchemaDetails;
21
+ })
22
+ .catch((error) => {
23
+ requests.delete(url);
24
+ throw error;
25
+ });
26
+ requests.set(url, request);
27
+ return request;
28
+ };
29
+ return Object.assign(load, { peek: (url: string) => resolved.get(url) });
30
+ };
31
+
32
+ export function useSchemaDetails(
33
+ item: SchemaItem | undefined,
34
+ load: ReturnType<typeof createSchemaDetailsLoader>,
35
+ enabled = true
36
+ ) {
37
+ const url = enabled ? item?.contentUrl : undefined;
38
+ const [attempt, setAttempt] = useState(0);
39
+ const [state, setState] = useState<{ url: string; details?: SchemaDetails; error?: string }>();
40
+
41
+ useEffect(() => {
42
+ if (!url || load.peek(url)) return;
43
+ let current = true;
44
+ setState(undefined);
45
+ load(url).then(
46
+ (details) => current && setState({ url, details }),
47
+ () => current && setState({ url, error: 'Unable to load schema content. Please try again.' })
48
+ );
49
+ return () => {
50
+ current = false;
51
+ };
52
+ }, [url, load, attempt]);
53
+
54
+ const matchingState = state?.url === url ? state : undefined;
55
+ const details = url ? matchingState?.details || load.peek(url) : undefined;
56
+ const message = useMemo(
57
+ () => (item && details ? { ...item, ...details, data: { ...item.data, ...details.data } } : item),
58
+ [item, details]
59
+ );
60
+ return {
61
+ message,
62
+ loading: !!url && !details && !matchingState?.error,
63
+ error: details ? undefined : matchingState?.error,
64
+ retry: () => {
65
+ setState(undefined);
66
+ setAttempt((value) => value + 1);
67
+ },
68
+ };
69
+ }
@@ -41,6 +41,18 @@ export function extractServiceName(refId: string): string {
41
41
  return match ? match[1] : refId;
42
42
  }
43
43
 
44
+ /** Resolve both enriched collection entries and compact explorer references. */
45
+ export function getSchemaRelationshipReference(reference: {
46
+ id: string;
47
+ version?: string;
48
+ data?: { id: string; version: string };
49
+ }) {
50
+ return {
51
+ id: reference.data?.id ?? reference.id,
52
+ version: reference.data?.version ?? reference.version,
53
+ };
54
+ }
55
+
44
56
  export const getLanguageForHighlight = (extension?: string): string => {
45
57
  if (!extension) return 'json';
46
58
  const ext = extension.toLowerCase();
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const getMessageIdsWithFieldUsage = vi.fn();
4
+ const pageDataLoader = {
5
+ events: vi.fn(),
6
+ commands: vi.fn(),
7
+ queries: vi.fn(),
8
+ };
9
+
10
+ vi.mock('@utils/feature', () => ({
11
+ isSSR: () => false,
12
+ }));
13
+
14
+ vi.mock('@utils/collections/field-usage', () => ({
15
+ getMessageIdsWithFieldUsage: () => getMessageIdsWithFieldUsage(),
16
+ }));
17
+
18
+ vi.mock('@utils/page-loaders/page-data-loader', () => ({
19
+ pageDataLoader,
20
+ }));
21
+
22
+ import { Page } from './_index.data';
23
+
24
+ const event = (id: string) => ({
25
+ collection: 'events',
26
+ data: { id, version: '1.0.0', name: id },
27
+ });
28
+
29
+ describe('field-lineage static paths', () => {
30
+ beforeEach(() => {
31
+ getMessageIdsWithFieldUsage.mockReset();
32
+ pageDataLoader.events.mockReset();
33
+ pageDataLoader.commands.mockReset();
34
+ pageDataLoader.queries.mockReset();
35
+ });
36
+
37
+ it('when we build a catalog, field usage pages are only created for messages that declare fields', async () => {
38
+ getMessageIdsWithFieldUsage.mockResolvedValue(new Set(['OrderCreated']));
39
+ pageDataLoader.events.mockResolvedValue([event('OrderCreated'), event('OrderCancelled')]);
40
+ pageDataLoader.commands.mockResolvedValue([]);
41
+ pageDataLoader.queries.mockResolvedValue([]);
42
+
43
+ const paths = await Page.getStaticPaths();
44
+
45
+ expect(paths).toEqual([
46
+ {
47
+ params: { type: 'events', id: 'OrderCreated', version: '1.0.0' },
48
+ props: { type: 'events', ...event('OrderCreated') },
49
+ },
50
+ ]);
51
+ });
52
+ });
@@ -1,6 +1,7 @@
1
1
  import { isSSR } from '@utils/feature';
2
2
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
3
  import type { PageTypes } from '@types';
4
+ import { getMessageIdsWithFieldUsage } from '@utils/collections/field-usage';
4
5
 
5
6
  const MESSAGE_TYPES: PageTypes[] = ['events', 'commands', 'queries'];
6
7
 
@@ -15,21 +16,24 @@ export class Page extends HybridPage {
15
16
  }
16
17
 
17
18
  const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
19
+ const messagesWithFieldUsage = await getMessageIdsWithFieldUsage();
18
20
 
19
21
  const allItems = await Promise.all(MESSAGE_TYPES.map((type) => pageDataLoader[type]()));
20
22
 
21
23
  return allItems.flatMap((items, index) =>
22
- items.map((item) => ({
23
- params: {
24
- type: MESSAGE_TYPES[index],
25
- id: item.data.id,
26
- version: item.data.version,
27
- },
28
- props: {
29
- type: MESSAGE_TYPES[index],
30
- ...item,
31
- },
32
- }))
24
+ items
25
+ .filter((item) => messagesWithFieldUsage.has(item.data.id))
26
+ .map((item) => ({
27
+ params: {
28
+ type: MESSAGE_TYPES[index],
29
+ id: item.data.id,
30
+ version: item.data.version,
31
+ },
32
+ props: {
33
+ type: MESSAGE_TYPES[index],
34
+ ...item,
35
+ },
36
+ }))
33
37
  );
34
38
  }
35
39
 
@@ -40,6 +44,11 @@ export class Page extends HybridPage {
40
44
  return null;
41
45
  }
42
46
 
47
+ const messagesWithFieldUsage = await getMessageIdsWithFieldUsage();
48
+ if (!messagesWithFieldUsage.has(id)) {
49
+ return null;
50
+ }
51
+
43
52
  const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
44
53
 
45
54
  const items = await pageDataLoader[type as PageTypes]();