@eventcatalog/core 3.34.0 → 3.35.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 (28) 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-IO6EMN5C.js → chunk-B2LDVIVY.js} +1 -1
  6. package/dist/{chunk-BJWMR3ZH.js → chunk-DFLUDECO.js} +1 -1
  7. package/dist/{chunk-K5AM6PPU.js → chunk-LUWCWNOR.js} +1 -1
  8. package/dist/{chunk-623CFR4T.js → chunk-NEWQKEP7.js} +1 -1
  9. package/dist/{chunk-PP7EDIPX.js → chunk-Y5O6SCX3.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/Search/SearchModal.tsx +23 -34
  19. package/eventcatalog/src/components/Search/search-utils.spec.ts +36 -0
  20. package/eventcatalog/src/components/Search/search-utils.ts +34 -0
  21. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +7 -1
  22. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +2 -0
  23. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +77 -0
  24. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +10 -1
  25. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +10 -3
  26. package/eventcatalog/src/stores/sidebar-store/state.ts +126 -2
  27. package/eventcatalog/src/utils/collections/flows.ts +3 -2
  28. package/package.json +3 -3
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "3.34.0";
40
+ var version = "3.35.0";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-IO6EMN5C.js";
4
- import "../chunk-623CFR4T.js";
3
+ } from "../chunk-B2LDVIVY.js";
4
+ import "../chunk-NEWQKEP7.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -111,7 +111,7 @@ var import_axios = __toESM(require("axios"), 1);
111
111
  var import_os = __toESM(require("os"), 1);
112
112
 
113
113
  // package.json
114
- var version = "3.34.0";
114
+ var version = "3.35.0";
115
115
 
116
116
  // src/constants.ts
117
117
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-BJWMR3ZH.js";
4
- import "../chunk-IO6EMN5C.js";
3
+ } from "../chunk-DFLUDECO.js";
4
+ import "../chunk-B2LDVIVY.js";
5
5
  import "../chunk-4UVFXLPI.js";
6
- import "../chunk-623CFR4T.js";
6
+ import "../chunk-NEWQKEP7.js";
7
7
  import "../chunk-5T63CXKU.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-623CFR4T.js";
3
+ } from "./chunk-NEWQKEP7.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-IO6EMN5C.js";
3
+ } from "./chunk-B2LDVIVY.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-623CFR4T.js";
3
+ } from "./chunk-NEWQKEP7.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "3.34.0";
2
+ var version = "3.35.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-K5AM6PPU.js";
3
+ } from "./chunk-LUWCWNOR.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.34.0";
28
+ var version = "3.35.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-623CFR4T.js";
3
+ } from "./chunk-NEWQKEP7.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -114,7 +114,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
114
114
  var import_picocolors = __toESM(require("picocolors"), 1);
115
115
 
116
116
  // package.json
117
- var version = "3.34.0";
117
+ var version = "3.35.0";
118
118
 
119
119
  // src/constants.ts
120
120
  var VERSION = version;
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-K3ZVEX2Y.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-BJWMR3ZH.js";
10
- import "./chunk-IO6EMN5C.js";
9
+ } from "./chunk-DFLUDECO.js";
10
+ import "./chunk-B2LDVIVY.js";
11
11
  import "./chunk-4UVFXLPI.js";
12
12
  import {
13
13
  runMigrations
@@ -22,13 +22,13 @@ import {
22
22
  } from "./chunk-3KXCGYET.js";
23
23
  import {
24
24
  generate
25
- } from "./chunk-PP7EDIPX.js";
25
+ } from "./chunk-Y5O6SCX3.js";
26
26
  import {
27
27
  logger
28
- } from "./chunk-K5AM6PPU.js";
28
+ } from "./chunk-LUWCWNOR.js";
29
29
  import {
30
30
  VERSION
31
- } from "./chunk-623CFR4T.js";
31
+ } from "./chunk-NEWQKEP7.js";
32
32
  import {
33
33
  getEventCatalogConfigFile,
34
34
  verifyRequiredFieldsAreInCatalogConfigFile
package/dist/generate.cjs CHANGED
@@ -78,7 +78,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
78
78
  var import_picocolors = __toESM(require("picocolors"), 1);
79
79
 
80
80
  // package.json
81
- var version = "3.34.0";
81
+ var version = "3.35.0";
82
82
 
83
83
  // src/constants.ts
84
84
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-PP7EDIPX.js";
4
- import "./chunk-K5AM6PPU.js";
5
- import "./chunk-623CFR4T.js";
3
+ } from "./chunk-Y5O6SCX3.js";
4
+ import "./chunk-LUWCWNOR.js";
5
+ import "./chunk-NEWQKEP7.js";
6
6
  import "./chunk-5T63CXKU.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.34.0";
39
+ var version = "3.35.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-K5AM6PPU.js";
4
- import "../chunk-623CFR4T.js";
3
+ } from "../chunk-LUWCWNOR.js";
4
+ import "../chunk-NEWQKEP7.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -27,6 +27,7 @@ import { useStore } from '@nanostores/react';
27
27
  import { favoritesStore, toggleFavorite as toggleFavoriteAction } from '../../stores/favorites-store';
28
28
  import { buildUrl } from '@utils/url-builder';
29
29
  import { resolveIconUrl } from '@utils/icon';
30
+ import { getUrlForSearchItem } from './search-utils';
30
31
 
31
32
  const typeIcons: any = {
32
33
  Domain: RectangleGroupIcon,
@@ -43,6 +44,8 @@ const typeIcons: any = {
43
44
  AsyncAPI: DocumentTextIcon,
44
45
  Design: Square2StackIcon,
45
46
  Container: CircleStackIcon,
47
+ 'Data Product': CubeIcon,
48
+ Flow: QueueListIcon,
46
49
  default: DocumentTextIcon,
47
50
  };
48
51
 
@@ -62,6 +65,8 @@ const typeColors: any = {
62
65
  AsyncAPI: 'text-violet-500 dark:text-violet-400 bg-violet-50 dark:bg-violet-500/10 ring-violet-200 dark:ring-violet-500/30',
63
66
  Design: 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
64
67
  Container: 'text-indigo-500 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 ring-indigo-200 dark:ring-indigo-500/30',
68
+ 'Data Product': 'text-sky-500 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 ring-sky-200 dark:ring-sky-500/30',
69
+ Flow: 'text-fuchsia-500 dark:text-fuchsia-400 bg-fuchsia-50 dark:bg-fuchsia-500/10 ring-fuchsia-200 dark:ring-fuchsia-500/30',
65
70
  default: 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
66
71
  };
67
72
 
@@ -69,35 +74,6 @@ function classNames(...classes: (string | boolean | undefined)[]) {
69
74
  return classes.filter(Boolean).join(' ');
70
75
  }
71
76
 
72
- // Helper to construct URL from key if href is missing
73
- const getUrlForItem = (node: any, key: string) => {
74
- const parts = key.split(':');
75
- if (parts.length < 2) return null; // Need at least type:id
76
-
77
- const type = parts[0];
78
- const id = parts[1];
79
- const version = parts[2]; // May be undefined
80
-
81
- // Skip list items and other special keys
82
- if (type === 'list') return null;
83
-
84
- // Only show items that have a version to avoid duplicates
85
- if (!version) return null;
86
-
87
- // If node has href, use it, otherwise construct from key
88
- if (node.href) return node.href;
89
-
90
- // Pluralize type for URL if needed
91
- let pluralType = type;
92
- if (['event', 'command', 'domain', 'service', 'flow', 'container', 'channel'].includes(type)) {
93
- pluralType = type + 's';
94
- } else if (type === 'query') {
95
- pluralType = 'queries';
96
- }
97
-
98
- return buildUrl(`/docs/${pluralType}/${id}/${version}`);
99
- };
100
-
101
77
  interface SearchNode {
102
78
  key: string;
103
79
  title: string;
@@ -203,7 +179,7 @@ export default function SearchModal() {
203
179
  const items = useMemo(() => {
204
180
  return searchNodes
205
181
  .map((node) => {
206
- const url = getUrlForItem(node as any, node.key);
182
+ const url = getUrlForSearchItem(node as any, node.key);
207
183
  if (!url) return null;
208
184
 
209
185
  return {
@@ -252,8 +228,11 @@ export default function SearchModal() {
252
228
  Message: 0,
253
229
  Team: 0,
254
230
  Container: 0,
231
+ Entity: 0,
255
232
  Design: 0,
256
233
  Channel: 0,
234
+ Flow: 0,
235
+ 'Data Product': 0,
257
236
  };
258
237
 
259
238
  itemsToCount.forEach((item) => {
@@ -277,8 +256,12 @@ export default function SearchModal() {
277
256
  if (counts.Domain > 0) dynamicFilters.push({ id: 'Domain', name: `Domains (${counts.Domain})` });
278
257
  if (counts.Service > 0) dynamicFilters.push({ id: 'Service', name: `Services (${counts.Service})` });
279
258
  if (counts.Message > 0) dynamicFilters.push({ id: 'Message', name: `Messages (${counts.Message})` });
280
- if (counts.Container > 0) dynamicFilters.push({ id: 'Container', name: `Containers (${counts.Container})` });
259
+ if (counts.Container > 0) dynamicFilters.push({ id: 'Container', name: `Data Stores (${counts.Container})` });
260
+ if (counts.Entity > 0) dynamicFilters.push({ id: 'Entity', name: `Entities (${counts.Entity})` });
281
261
  if (counts.Channel > 0) dynamicFilters.push({ id: 'Channel', name: `Channels (${counts.Channel})` });
262
+ if (counts.Flow > 0) dynamicFilters.push({ id: 'Flow', name: `Flows (${counts.Flow})` });
263
+ if (counts['Data Product'] > 0)
264
+ dynamicFilters.push({ id: 'Data Product', name: `Data Products (${counts['Data Product']})` });
282
265
  if (counts.Design > 0) dynamicFilters.push({ id: 'Design', name: `Designs (${counts.Design})` });
283
266
  if (counts.Team > 0) dynamicFilters.push({ id: 'Team', name: `Teams & Users (${counts.Team})` });
284
267
 
@@ -317,7 +300,7 @@ export default function SearchModal() {
317
300
  .slice(0, 5)
318
301
  .map((fav) => {
319
302
  const node = searchNodeLookup.get(fav.nodeKey);
320
- const url = node ? getUrlForItem(node as any, fav.nodeKey) : fav.href;
303
+ const url = node ? getUrlForSearchItem(node as any, fav.nodeKey) : fav.href;
321
304
  if (!url) return null;
322
305
 
323
306
  return {
@@ -411,7 +394,13 @@ export default function SearchModal() {
411
394
  </div>
412
395
 
413
396
  {/* Filter Tabs */}
414
- <div className="flex items-center gap-2 px-4 py-3 overflow-x-auto no-scrollbar border-b border-[rgb(var(--ec-page-border))]">
397
+ <div
398
+ className="flex items-center gap-2 px-4 pt-3 pb-3.5 overflow-x-auto overscroll-x-contain border-b border-[rgb(var(--ec-page-border))]"
399
+ style={{
400
+ scrollbarWidth: 'thin',
401
+ scrollbarColor: 'rgb(var(--ec-page-border)) transparent',
402
+ }}
403
+ >
415
404
  {filters.map((tab) => (
416
405
  <button
417
406
  key={tab.id}
@@ -566,7 +555,7 @@ export default function SearchModal() {
566
555
  <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]" />
567
556
  <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search for anything</p>
568
557
  <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">
569
- Search for domains, services, events, commands, queries and more.
558
+ Search for domains, services, events, commands, queries, data stores, data products, flows and more.
570
559
  </p>
571
560
  </div>
572
561
  )}
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import { getUrlForSearchItem } from './search-utils';
3
+
4
+ declare global {
5
+ interface Window {
6
+ __EC_TRAILING_SLASH__: boolean;
7
+ }
8
+ }
9
+
10
+ describe('getUrlForSearchItem', () => {
11
+ beforeEach(() => {
12
+ // @ts-ignore
13
+ global.__EC_TRAILING_SLASH__ = false;
14
+ });
15
+
16
+ it.each([
17
+ ['container:OrdersDatabase:1.0.0', '/docs/containers/OrdersDatabase/1.0.0'],
18
+ ['data-product:Customer360:1.0.0', '/docs/data-products/Customer360/1.0.0'],
19
+ ['entity:Order:1.0.0', '/docs/entities/Order/1.0.0'],
20
+ ['flow:CheckoutFlow:1.0.0', '/docs/flows/CheckoutFlow/1.0.0'],
21
+ ])('builds a docs URL for %s', (key, expected) => {
22
+ expect(getUrlForSearchItem({}, key)).toBe(expected);
23
+ });
24
+
25
+ it('uses the explicit node href when provided', () => {
26
+ expect(getUrlForSearchItem({ href: '/custom/search-result' }, 'data-product:Customer360:1.0.0')).toBe(
27
+ '/custom/search-result'
28
+ );
29
+ });
30
+
31
+ it('skips aliases and unsupported keys', () => {
32
+ expect(getUrlForSearchItem({}, 'data-product:Customer360')).toBeNull();
33
+ expect(getUrlForSearchItem({}, 'list:data-products')).toBeNull();
34
+ expect(getUrlForSearchItem({}, 'unknown:Customer360:1.0.0')).toBeNull();
35
+ });
36
+ });
@@ -0,0 +1,34 @@
1
+ import { buildUrl } from '@utils/url-builder';
2
+
3
+ const docsPathByType: Record<string, string> = {
4
+ channel: 'channels',
5
+ command: 'commands',
6
+ container: 'containers',
7
+ 'data-product': 'data-products',
8
+ domain: 'domains',
9
+ entity: 'entities',
10
+ event: 'events',
11
+ flow: 'flows',
12
+ query: 'queries',
13
+ service: 'services',
14
+ };
15
+
16
+ export const getUrlForSearchItem = (node: { href?: string }, key: string) => {
17
+ const parts = key.split(':');
18
+ if (parts.length < 2) return null;
19
+
20
+ const type = parts[0];
21
+ const id = parts[1];
22
+ const version = parts[2];
23
+
24
+ if (type === 'list') return null;
25
+
26
+ if (!version) return null;
27
+
28
+ if (node.href) return node.href;
29
+
30
+ const docsPath = docsPathByType[type];
31
+ if (!docsPath) return null;
32
+
33
+ return buildUrl(`/docs/${docsPath}/${id}/${version}`);
34
+ };
@@ -16,6 +16,8 @@ import {
16
16
  SquareMousePointer,
17
17
  ListOrdered,
18
18
  ArrowLeftRight,
19
+ Package,
20
+ Box,
19
21
  } from 'lucide-react';
20
22
  import type { NavNode } from '@stores/sidebar-store/state';
21
23
  import { getBadgeClasses } from './utils';
@@ -83,8 +85,10 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
83
85
  { key: 'channel', label: 'Channels', badge: 'Channel', icon: ArrowLeftRight },
84
86
  { key: 'command', label: 'Commands', badge: 'Command', icon: MessageSquare },
85
87
  { key: 'container', label: 'Data Stores', badge: 'Container', icon: Database },
88
+ { key: 'data-product', label: 'Data Products', badge: 'Data Product', icon: Package },
86
89
  { key: 'design', label: 'Designs', badge: 'Design', icon: SquareMousePointer },
87
90
  { key: 'domain', label: 'Domains', badge: 'Domain', icon: Boxes },
91
+ { key: 'entity', label: 'Entities', badge: 'Entity', icon: Box },
88
92
  { key: 'event', label: 'Events', badge: 'Event', icon: Zap },
89
93
  { key: 'flow', label: 'Flows', badge: 'Flow', icon: Waypoints },
90
94
  { key: 'query', label: 'Queries', badge: 'Query', icon: SearchIcon },
@@ -116,9 +120,11 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
116
120
  Command: 'command',
117
121
  Query: 'query',
118
122
  Container: 'container',
123
+ 'Data Product': 'data-product',
119
124
  Flow: 'flow',
120
125
  Design: 'design',
121
126
  Channel: 'channel',
127
+ Entity: 'entity',
122
128
  };
123
129
 
124
130
  // Use the memoized array instead of Object.entries(nodes)
@@ -135,7 +141,7 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
135
141
 
136
142
  const keyParts = key.split(':');
137
143
  if (keyParts.length >= 3) {
138
- const id = keyParts[2].toLowerCase();
144
+ const id = keyParts[1].toLowerCase();
139
145
  if (id.includes(query)) {
140
146
  results.push({ nodeKey: key, node, matchType: 'id' });
141
147
  }
@@ -239,6 +239,8 @@ export default function NestedSideBar() {
239
239
  // Data Products
240
240
  { pattern: /^\/docs\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
241
241
  { pattern: /^\/visualiser\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
242
+ // Entities
243
+ { pattern: /^\/docs\/entities\/([^/]+)\/([^/]+)/, type: 'entity' },
242
244
  ];
243
245
 
244
246
  // URL patterns without version (language pages, etc)
@@ -3,6 +3,44 @@ import { buildUrl } from '@utils/url-builder';
3
3
  import type { NavNode, ChildRef, ResourceGroupContext } from './shared';
4
4
  import { buildQuickReferenceSection, buildResourceDocsSection, shouldRenderSideBarSection } from './shared';
5
5
  import { isChangelogEnabled } from '@utils/feature';
6
+ import { createVersionedMap, findInMap } from '@utils/collections/util';
7
+ import { pluralizeMessageType } from '@utils/collections/messages';
8
+
9
+ type VersionedEntry = { collection?: string; data: { id: string; version: string } };
10
+ type VersionedEntryMap<T extends VersionedEntry> = Map<string, T[]>;
11
+
12
+ const uniqueRefs = (refs: string[]) => [...new Set(refs)];
13
+
14
+ const resolvePointer = <T extends VersionedEntry>(
15
+ map: VersionedEntryMap<T>,
16
+ pointer: { id: string; version?: string }
17
+ ): T | undefined => {
18
+ return findInMap(map, pointer.id, pointer.version);
19
+ };
20
+
21
+ const resolveMessageStep = (
22
+ step: any,
23
+ maps: {
24
+ eventMap: VersionedEntryMap<CollectionEntry<'events'>>;
25
+ commandMap: VersionedEntryMap<CollectionEntry<'commands'>>;
26
+ queryMap: VersionedEntryMap<CollectionEntry<'queries'>>;
27
+ }
28
+ ): string | null => {
29
+ if (!step.message) return null;
30
+
31
+ const hydratedMessage = Array.isArray(step.message) ? step.message[0] : undefined;
32
+ if (hydratedMessage?.collection && hydratedMessage?.data) {
33
+ return `${pluralizeMessageType(hydratedMessage as any)}:${hydratedMessage.data.id}:${hydratedMessage.data.version}`;
34
+ }
35
+
36
+ const pointer = Array.isArray(step.message) ? undefined : step.message;
37
+ if (!pointer?.id) return null;
38
+
39
+ const message =
40
+ resolvePointer(maps.eventMap, pointer) || resolvePointer(maps.commandMap, pointer) || resolvePointer(maps.queryMap, pointer);
41
+
42
+ return message ? `${pluralizeMessageType(message as any)}:${message.data.id}:${message.data.version}` : null;
43
+ };
6
44
 
7
45
  export const buildFlowNode = (flow: CollectionEntry<'flows'>, context: ResourceGroupContext): NavNode => {
8
46
  const docsSection = buildResourceDocsSection(
@@ -12,6 +50,27 @@ export const buildFlowNode = (flow: CollectionEntry<'flows'>, context: ResourceG
12
50
  context.resourceDocs,
13
51
  context.resourceDocCategories
14
52
  );
53
+ const steps = flow.data.steps || [];
54
+ const eventMap = createVersionedMap(context.events);
55
+ const commandMap = createVersionedMap(context.commands);
56
+ const queryMap = createVersionedMap(context.queries);
57
+ const serviceMap = createVersionedMap(context.services);
58
+ const flowMap = createVersionedMap(context.flows);
59
+ const messageRefs = uniqueRefs(
60
+ steps.map((step) => resolveMessageStep(step, { eventMap, commandMap, queryMap })).filter(Boolean) as string[]
61
+ );
62
+ const serviceRefs = uniqueRefs(
63
+ steps
64
+ .map((step) => (step.service ? resolvePointer(serviceMap, step.service) : undefined))
65
+ .filter(Boolean)
66
+ .map((service) => `service:${service!.data.id}:${service!.data.version}`)
67
+ );
68
+ const flowRefs = uniqueRefs(
69
+ steps
70
+ .map((step) => (step.flow ? resolvePointer(flowMap, step.flow) : undefined))
71
+ .filter(Boolean)
72
+ .map((referencedFlow) => `flow:${referencedFlow!.data.id}:${referencedFlow!.data.version}`)
73
+ );
15
74
 
16
75
  return {
17
76
  type: 'item',
@@ -43,6 +102,24 @@ export const buildFlowNode = (flow: CollectionEntry<'flows'>, context: ResourceG
43
102
  },
44
103
  ].filter(Boolean) as ChildRef[],
45
104
  },
105
+ messageRefs.length > 0 && {
106
+ type: 'group',
107
+ title: 'Messages',
108
+ icon: 'Mail',
109
+ pages: messageRefs,
110
+ },
111
+ serviceRefs.length > 0 && {
112
+ type: 'group',
113
+ title: 'Services',
114
+ icon: 'Server',
115
+ pages: serviceRefs,
116
+ },
117
+ flowRefs.length > 0 && {
118
+ type: 'group',
119
+ title: 'Subflows',
120
+ icon: 'Waypoints',
121
+ pages: flowRefs,
122
+ },
46
123
  ].filter(Boolean) as ChildRef[],
47
124
  };
48
125
  };
@@ -18,7 +18,8 @@ export const buildMessageNode = (
18
18
  message: CollectionEntry<'events' | 'commands' | 'queries'>,
19
19
  owners: any[],
20
20
  context: ResourceGroupContext,
21
- hasFieldUsage: boolean = false
21
+ hasFieldUsage: boolean = false,
22
+ flowRefs: string[] = []
22
23
  ): NavNode => {
23
24
  const producers = message.data.producers || [];
24
25
  const consumers = message.data.consumers || [];
@@ -26,6 +27,7 @@ export const buildMessageNode = (
26
27
 
27
28
  const renderProducers = producers.length > 0 && shouldRenderSideBarSection(message, 'producers');
28
29
  const renderConsumers = consumers.length > 0 && shouldRenderSideBarSection(message, 'consumers');
30
+ const renderFlows = flowRefs.length > 0 && shouldRenderSideBarSection(message, 'flows');
29
31
  const renderRepository = message.data.repository && shouldRenderSideBarSection(message, 'repository');
30
32
 
31
33
  // Determine badge based on collection type
@@ -132,6 +134,13 @@ export const buildMessageNode = (
132
134
  pages: consumers.map((consumer) => `service:${(consumer as any).data.id}:${(consumer as any).data.version}`),
133
135
  visible: consumers.length > 0,
134
136
  },
137
+ renderFlows && {
138
+ type: 'group',
139
+ title: 'Flows',
140
+ icon: 'Waypoints',
141
+ pages: flowRefs,
142
+ visible: flowRefs.length > 0,
143
+ },
135
144
  renderOwners && buildOwnersSection(owners),
136
145
  renderRepository && buildRepositorySection(message.data.repository as { url: string; language: string }),
137
146
  hasAttachments && buildAttachmentsSection(message.data.attachments as any[]),
@@ -17,11 +17,14 @@ import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
17
17
  import { pluralizeMessageType } from '@utils/collections/messages';
18
18
  import { iconFieldsForResource } from '@utils/icon';
19
19
 
20
+ const uniqueRefs = (refs: string[]) => [...new Set(refs)];
21
+
20
22
  export const buildServiceNode = (
21
23
  service: CollectionEntry<'services'>,
22
24
  owners: any[],
23
25
  context: ResourceGroupContext,
24
- serviceChannels: CollectionEntry<'channels'>[] = []
26
+ serviceChannels: CollectionEntry<'channels'>[] = [],
27
+ flowRefs: string[] = []
25
28
  ): NavNode => {
26
29
  const sendsMessages = service.data.sends || [];
27
30
  const receivesMessages = service.data.receives || [];
@@ -36,7 +39,11 @@ export const buildServiceNode = (
36
39
  const dataStoresInService = uniqueBy([...(service.data.writesTo || []), ...(service.data.readsFrom || [])], 'id');
37
40
 
38
41
  const serviceFlows = service.data.flows || [];
39
- const hasFlows = serviceFlows.length > 0;
42
+ const serviceFlowRefs = uniqueRefs([
43
+ ...serviceFlows.map((flow) => `flow:${(flow as any).data.id}:${(flow as any).data.version}`),
44
+ ...flowRefs,
45
+ ]);
46
+ const hasFlows = serviceFlowRefs.length > 0;
40
47
 
41
48
  const hasAttachments = service.data.attachments && service.data.attachments.length > 0;
42
49
 
@@ -197,7 +204,7 @@ export const buildServiceNode = (
197
204
  type: 'group',
198
205
  title: 'Flows',
199
206
  icon: 'Waypoints',
200
- pages: serviceFlows.map((flow) => `flow:${(flow as any).data.id}:${(flow as any).data.version}`),
207
+ pages: serviceFlowRefs,
201
208
  },
202
209
  renderOwners && buildOwnersSection(owners),
203
210
  renderRepository && buildRepositorySection(service.data.repository as { url: string; language: string }),
@@ -10,6 +10,7 @@ import { getUsers } from '@utils/collections/users';
10
10
  import { getTeams } from '@utils/collections/teams';
11
11
  import { getDiagrams } from '@utils/collections/diagrams';
12
12
  import { getDataProducts } from '@utils/collections/data-products';
13
+ import { getEntities } from '@utils/collections/entities';
13
14
  import { getResourceDocCategories, getResourceDocs } from '@utils/collections/resource-docs';
14
15
  import { buildUrl } from '@utils/url-builder';
15
16
  import type { NavigationData, NavNode, ChildRef } from './builders/shared';
@@ -32,6 +33,92 @@ export type { NavigationData, NavNode, ChildRef };
32
33
  const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
33
34
  let memoryCache: NavigationData | null = null;
34
35
 
36
+ type MessageEntry = CollectionEntry<'events' | 'commands' | 'queries'>;
37
+ type ServiceEntry = CollectionEntry<'services'>;
38
+
39
+ const getMessageNodeKey = (message: MessageEntry) =>
40
+ `${pluralizeMessageType(message)}:${message.data.id}:${message.data.version}`;
41
+
42
+ const uniqueRefs = (refs: string[]) => [...new Set(refs)];
43
+
44
+ const buildFlowReferencesByMessage = ({
45
+ flows,
46
+ events,
47
+ commands,
48
+ queries,
49
+ }: {
50
+ flows: CollectionEntry<'flows'>[];
51
+ events: CollectionEntry<'events'>[];
52
+ commands: CollectionEntry<'commands'>[];
53
+ queries: CollectionEntry<'queries'>[];
54
+ }) => {
55
+ const eventMap = createVersionedMap(events);
56
+ const commandMap = createVersionedMap(commands);
57
+ const queryMap = createVersionedMap(queries);
58
+ const flowRefsByMessage = new Map<string, string[]>();
59
+
60
+ const addFlowRef = (message: MessageEntry, flow: CollectionEntry<'flows'>) => {
61
+ const messageKey = getMessageNodeKey(message);
62
+ const flowKey = `flow:${flow.data.id}:${flow.data.version}`;
63
+ flowRefsByMessage.set(messageKey, uniqueRefs([...(flowRefsByMessage.get(messageKey) || []), flowKey]));
64
+ };
65
+
66
+ const resolveMessagePointer = (pointer: { id: string; version?: string }): MessageEntry | undefined => {
67
+ return (
68
+ findInMap(eventMap, pointer.id, pointer.version) ||
69
+ findInMap(commandMap, pointer.id, pointer.version) ||
70
+ findInMap(queryMap, pointer.id, pointer.version)
71
+ );
72
+ };
73
+
74
+ for (const flow of flows) {
75
+ for (const step of flow.data.steps || []) {
76
+ if (!step.message) continue;
77
+
78
+ const hydratedMessage = Array.isArray(step.message) ? step.message[0] : undefined;
79
+ if (hydratedMessage?.collection && hydratedMessage?.data) {
80
+ addFlowRef(hydratedMessage as MessageEntry, flow);
81
+ continue;
82
+ }
83
+
84
+ if (Array.isArray(step.message)) continue;
85
+
86
+ const message = resolveMessagePointer(step.message);
87
+ if (message) addFlowRef(message, flow);
88
+ }
89
+ }
90
+
91
+ return flowRefsByMessage;
92
+ };
93
+
94
+ const buildFlowReferencesByService = ({
95
+ flows,
96
+ services,
97
+ }: {
98
+ flows: CollectionEntry<'flows'>[];
99
+ services: CollectionEntry<'services'>[];
100
+ }) => {
101
+ const serviceMap = createVersionedMap(services);
102
+ const flowRefsByService = new Map<string, string[]>();
103
+
104
+ const addFlowRef = (service: ServiceEntry, flow: CollectionEntry<'flows'>) => {
105
+ const serviceKey = `service:${service.data.id}:${service.data.version}`;
106
+ const flowKey = `flow:${flow.data.id}:${flow.data.version}`;
107
+ flowRefsByService.set(serviceKey, uniqueRefs([...(flowRefsByService.get(serviceKey) || []), flowKey]));
108
+ };
109
+
110
+ for (const flow of flows) {
111
+ for (const step of flow.data.steps || []) {
112
+ if (!step.service) continue;
113
+
114
+ const service = findInMap(serviceMap, step.service.id, step.service.version);
115
+ if (service) addFlowRef(service, flow);
116
+ }
117
+ }
118
+
119
+ return flowRefsByService;
120
+ };
121
+
35
122
  /**
36
123
  * Get the navigation data for the sidebar
37
124
  */
@@ -52,6 +139,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
52
139
  channels,
53
140
  diagrams,
54
141
  dataProducts,
142
+ entities,
55
143
  resourceDocs,
56
144
  resourceDocCategories,
57
145
  ] = await Promise.all([
@@ -66,6 +154,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
66
154
  getChannels({ getAllVersions: false }),
67
155
  getDiagrams({ getAllVersions: false }),
68
156
  getDataProducts({ getAllVersions: false }),
157
+ getEntities({ getAllVersions: false }),
69
158
  getResourceDocs(),
70
159
  getResourceDocCategories(),
71
160
  ]);
@@ -86,6 +175,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
86
175
  containers,
87
176
  diagrams,
88
177
  dataProducts,
178
+ entities,
89
179
  resourceDocs,
90
180
  resourceDocCategories,
91
181
  };
@@ -190,11 +280,13 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
190
280
  }
191
281
  }
192
282
 
283
+ const flowRefsByService = buildFlowReferencesByService({ flows, services });
284
+
193
285
  const serviceNodes = servicesWithOwners.reduce(
194
286
  (acc, { service, owners }) => {
195
287
  const versionedKey = `service:${service.data.id}:${service.data.version}`;
196
288
  const serviceChannels = serviceChannelsMap.get(`${service.data.id}:${service.data.version}`) || [];
197
- acc[versionedKey] = buildServiceNode(service, owners, context, serviceChannels);
289
+ acc[versionedKey] = buildServiceNode(service, owners, context, serviceChannels, flowRefsByService.get(versionedKey) || []);
198
290
  if (service.data.latestVersion === service.data.version) {
199
291
  // Store reference to versioned key instead of duplicating the full node
200
292
  acc[`service:${service.data.id}`] = versionedKey;
@@ -217,12 +309,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
217
309
  }
218
310
  }
219
311
 
312
+ const flowRefsByMessage = buildFlowReferencesByMessage({ flows, events, commands, queries });
313
+
220
314
  const messageNodes = messagesWithOwners.reduce(
221
315
  (acc, { message, owners }) => {
222
316
  const type = pluralizeMessageType(message as any);
223
317
  const versionedKey = `${type}:${message.data.id}:${message.data.version}`;
224
318
  const hasFieldUsage = messagesWithFieldUsage.has(message.data.id);
225
- acc[versionedKey] = buildMessageNode(message, owners, context, hasFieldUsage);
319
+ acc[versionedKey] = buildMessageNode(message, owners, context, hasFieldUsage, flowRefsByMessage.get(versionedKey) || []);
226
320
  if (message.data.latestVersion === message.data.version) {
227
321
  // Store reference to versioned key instead of duplicating the full node
228
322
  acc[`${type}:${message.data.id}`] = versionedKey;
@@ -276,6 +370,25 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
276
370
  {} as Record<string, NavNode | string>
277
371
  );
278
372
 
373
+ const entityNodes = entities.reduce(
374
+ (acc, entity) => {
375
+ const versionedKey = `entity:${entity.data.id}:${entity.data.version}`;
376
+ acc[versionedKey] = {
377
+ type: 'item',
378
+ title: entity.data.name,
379
+ badge: 'Entity',
380
+ summary: entity.data.summary,
381
+ icon: 'Box',
382
+ href: buildUrl(`/docs/entities/${entity.data.id}/${entity.data.version}`),
383
+ };
384
+ if (entity.data.latestVersion === entity.data.version) {
385
+ acc[`entity:${entity.data.id}`] = versionedKey;
386
+ }
387
+ return acc;
388
+ },
389
+ {} as Record<string, NavNode | string>
390
+ );
391
+
279
392
  const designNodes = designs.reduce(
280
393
  (acc, design) => {
281
394
  acc[`design:${design.data.id}`] = {
@@ -435,6 +548,13 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
435
548
  pages: dataProducts.map((dataProduct) => `data-product:${dataProduct.data.id}:${dataProduct.data.version}`),
436
549
  });
437
550
 
551
+ const entitiesList = createLeaf(entities, {
552
+ type: 'item',
553
+ title: 'Entities',
554
+ icon: 'Box',
555
+ pages: entities.map((entity) => `entity:${entity.data.id}:${entity.data.version}`),
556
+ });
557
+
438
558
  const designsList = createLeaf(designs, {
439
559
  type: 'item',
440
560
  title: 'Designs',
@@ -498,6 +618,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
498
618
  'list:flows',
499
619
  'list:containers',
500
620
  'list:data-products',
621
+ 'list:entities',
501
622
  'list:designs',
502
623
  'list:people',
503
624
  ];
@@ -510,6 +631,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
510
631
  flowsList,
511
632
  containersList,
512
633
  dataProductsList,
634
+ entitiesList,
513
635
  designsList,
514
636
  peopleList,
515
637
  ];
@@ -537,6 +659,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
537
659
  ...(flowsList ? { 'list:flows': flowsList } : {}),
538
660
  ...(containersList ? { 'list:containers': containersList } : {}),
539
661
  ...(dataProductsList ? { 'list:data-products': dataProductsList } : {}),
662
+ ...(entitiesList ? { 'list:entities': entitiesList } : {}),
540
663
  ...(designsList ? { 'list:designs': designsList } : {}),
541
664
  ...(teamsList ? { 'list:teams': teamsList } : {}),
542
665
  ...(usersList ? { 'list:users': usersList } : {}),
@@ -572,6 +695,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
572
695
  ...channelNodes,
573
696
  ...containerNodes,
574
697
  ...dataProductNodes,
698
+ ...entityNodes,
575
699
  ...flowNodes,
576
700
  ...userNodes,
577
701
  ...teamNodes,
@@ -24,13 +24,14 @@ export const getFlows = async ({ getAllVersions = true }: Props = {}): Promise<F
24
24
  }
25
25
 
26
26
  // 1. Fetch collections in parallel
27
- const [allFlows, allEvents, allCommands] = await Promise.all([
27
+ const [allFlows, allEvents, allCommands, allQueries] = await Promise.all([
28
28
  getCollection('flows'),
29
29
  getCollection('events'),
30
30
  getCollection('commands'),
31
+ getCollection('queries'),
31
32
  ]);
32
33
 
33
- const allMessages = [...allEvents, ...allCommands];
34
+ const allMessages = [...allEvents, ...allCommands, ...allQueries];
34
35
 
35
36
  // 2. Build optimized maps
36
37
  const flowMap = createVersionedMap(allFlows);
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "3.34.0",
10
+ "version": "3.35.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -68,7 +68,7 @@
68
68
  "elkjs": "^0.10.0",
69
69
  "glob": "^13.0.6",
70
70
  "gray-matter": "^4.0.3",
71
- "hono": "4.12.16",
71
+ "hono": "4.12.18",
72
72
  "html-to-image": "^1.11.11",
73
73
  "js-yaml": "^4.1.1",
74
74
  "jsonpath-plus": "^10.4.0",
@@ -105,8 +105,8 @@
105
105
  "update-notifier": "^7.3.1",
106
106
  "uuid": "^10.0.0",
107
107
  "zod": "^4.3.6",
108
- "@eventcatalog/sdk": "2.21.0",
109
108
  "@eventcatalog/linter": "1.0.22",
109
+ "@eventcatalog/sdk": "2.21.0",
110
110
  "@eventcatalog/visualiser": "^3.20.0"
111
111
  },
112
112
  "devDependencies": {