@eventcatalog/core 2.65.1 → 3.0.0-beta.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 (123) hide show
  1. package/README.md +1 -1
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/{chunk-2TTD2MLE.js → chunk-JB4YT5JY.js} +1 -1
  7. package/dist/{chunk-BTS6L3KY.js → chunk-TQ4HZREX.js} +1 -1
  8. package/dist/{chunk-XB4SZX3I.js → chunk-X4W4YC3U.js} +1 -1
  9. package/dist/constants.cjs +1 -1
  10. package/dist/constants.js +1 -1
  11. package/dist/eventcatalog.cjs +1 -21
  12. package/dist/eventcatalog.config.d.cts +9 -0
  13. package/dist/eventcatalog.config.d.ts +9 -0
  14. package/dist/eventcatalog.js +3 -20
  15. package/eventcatalog/src/components/CopyAsMarkdown.tsx +19 -1
  16. package/eventcatalog/src/components/FavoriteButton.tsx +54 -0
  17. package/eventcatalog/src/components/Grids/DomainGrid.tsx +386 -362
  18. package/eventcatalog/src/components/Grids/MessageGrid.tsx +166 -523
  19. package/eventcatalog/src/components/Header.astro +48 -23
  20. package/eventcatalog/src/components/Lists/VersionList.astro +2 -2
  21. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +8 -2
  22. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +37 -0
  23. package/eventcatalog/src/components/Search/Search.astro +48 -28
  24. package/eventcatalog/src/components/Search/SearchModal.tsx +393 -702
  25. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +298 -0
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/container.ts +66 -0
  27. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +101 -0
  28. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/flow.ts +29 -0
  29. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/message.ts +84 -0
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +147 -0
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +146 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +1073 -0
  33. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +365 -0
  34. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +90 -0
  35. package/eventcatalog/src/components/SideNav/SideNav.astro +18 -28
  36. package/eventcatalog/src/content.config.ts +2 -0
  37. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +3 -3
  38. package/eventcatalog/src/layouts/DirectoryLayout.astro +2 -2
  39. package/eventcatalog/src/layouts/DiscoverLayout.astro +3 -3
  40. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +83 -64
  41. package/eventcatalog/src/layouts/VisualiserLayout.astro +3 -3
  42. package/eventcatalog/src/pages/_index.astro +530 -110
  43. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +64 -0
  44. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +29 -0
  45. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +4 -4
  46. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -4
  47. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +3 -3
  48. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -5
  49. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +354 -186
  50. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +1 -1
  51. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +4 -4
  52. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +1 -4
  53. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +3 -27
  54. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +2 -2
  55. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +2 -2
  56. package/eventcatalog/src/pages/nav-index.json.ts +30 -0
  57. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +77 -0
  58. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +90 -0
  59. package/eventcatalog/src/pages/schemas/{index.astro → explorer/index.astro} +3 -3
  60. package/eventcatalog/src/pages/studio.astro +3 -3
  61. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +2 -2
  62. package/eventcatalog/src/stores/favorites-store.ts +83 -0
  63. package/eventcatalog/src/stores/sidebar-store.ts +8 -0
  64. package/eventcatalog/src/utils/collections/changelogs.ts +7 -4
  65. package/eventcatalog/src/utils/{channels.ts → collections/channels.ts} +81 -31
  66. package/eventcatalog/src/utils/collections/commands.ts +134 -0
  67. package/eventcatalog/src/utils/collections/containers.ts +44 -33
  68. package/eventcatalog/src/utils/collections/domains.ts +204 -62
  69. package/eventcatalog/src/utils/{entities.ts → collections/entities.ts} +44 -24
  70. package/eventcatalog/src/utils/collections/events.ts +136 -0
  71. package/eventcatalog/src/utils/collections/flows.ts +59 -25
  72. package/eventcatalog/src/utils/{messages.ts → collections/messages.ts} +13 -4
  73. package/eventcatalog/src/utils/{queries.ts → collections/queries.ts} +49 -28
  74. package/eventcatalog/src/utils/collections/services.ts +100 -68
  75. package/eventcatalog/src/utils/collections/teams.ts +94 -0
  76. package/eventcatalog/src/utils/collections/users.ts +122 -0
  77. package/eventcatalog/src/utils/collections/util.ts +57 -1
  78. package/eventcatalog/src/utils/feature.ts +2 -2
  79. package/eventcatalog/src/utils/{collections/file-diffs.ts → file-diffs.ts} +1 -1
  80. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -0
  81. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +16 -6
  82. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +14 -10
  83. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +36 -64
  84. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +23 -19
  85. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +36 -49
  86. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +22 -18
  87. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +4 -4
  88. package/eventcatalog/tailwind.config.mjs +14 -0
  89. package/eventcatalog/tsconfig.json +2 -1
  90. package/package.json +7 -4
  91. package/eventcatalog/public/logo_old.png +0 -0
  92. package/eventcatalog/src/components/DiscoverInsight.astro +0 -61
  93. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +0 -540
  94. package/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro +0 -55
  95. package/eventcatalog/src/components/Lists/ProtocolList.tsx +0 -74
  96. package/eventcatalog/src/components/Lists/RepositoryList.astro +0 -37
  97. package/eventcatalog/src/components/Lists/SpecificationsList.astro +0 -67
  98. package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +0 -204
  99. package/eventcatalog/src/components/SideBars/ContainerSideBar.astro +0 -183
  100. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +0 -277
  101. package/eventcatalog/src/components/SideBars/EntitySideBar.astro +0 -139
  102. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +0 -132
  103. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +0 -251
  104. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +0 -298
  105. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx +0 -46
  106. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +0 -78
  107. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx +0 -83
  108. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +0 -1250
  109. package/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +0 -91
  110. package/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +0 -201
  111. package/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +0 -190
  112. package/eventcatalog/src/components/SideNav/TreeView/index.tsx +0 -94
  113. package/eventcatalog/src/components/TreeView/index.tsx +0 -328
  114. package/eventcatalog/src/components/TreeView/styles.module.css +0 -264
  115. package/eventcatalog/src/components/TreeView/useSlots.ts +0 -95
  116. package/eventcatalog/src/pages/architecture/[type]/index.astro +0 -14
  117. package/eventcatalog/src/pages/architecture/architecture.astro +0 -110
  118. package/eventcatalog/src/pages/architecture/docs/[type]/index.astro +0 -14
  119. package/eventcatalog/src/utils/commands.ts +0 -112
  120. package/eventcatalog/src/utils/events.ts +0 -108
  121. package/eventcatalog/src/utils/generators/index.ts +0 -10
  122. package/eventcatalog/src/utils/teams.ts +0 -72
  123. package/eventcatalog/src/utils/users.ts +0 -72
@@ -1,81 +1,95 @@
1
- import { getItemsFromCollectionByIdAndSemverOrLatest, getVersionForCollectionItem } from '@utils/collections/util';
2
1
  import { getCollection } from 'astro:content';
3
2
  import type { CollectionEntry } from 'astro:content';
4
3
  import path from 'path';
5
4
  import semver from 'semver';
6
- import type { CollectionTypes } from '@types';
5
+ import type { CollectionMessageTypes, CollectionTypes } from '@types';
7
6
  const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
8
- import utils from '@eventcatalog/sdk';
7
+ import utils, { type Domain } from '@eventcatalog/sdk';
8
+ import { getDomains, getDomainsForService } from './domains';
9
+ import { createVersionedMap, findInMap } from '@utils/collections/util';
9
10
 
10
11
  export type Service = CollectionEntry<'services'>;
11
12
 
13
+ const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
12
14
  interface Props {
13
15
  getAllVersions?: boolean;
16
+ returnBody?: boolean;
14
17
  }
15
18
 
16
- // Cache for build time
17
- let cachedServices: Record<string, Service[]> = {
18
- allVersions: [],
19
- currentVersions: [],
20
- };
19
+ // Simple in-memory cache
20
+ let memoryCache: Record<string, Service[]> = {};
21
21
 
22
- export const getServices = async ({ getAllVersions = true }: Props = {}): Promise<Service[]> => {
23
- const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions';
22
+ export const getServices = async ({ getAllVersions = true, returnBody = false }: Props = {}): Promise<Service[]> => {
23
+ // console.time(' New getServices');
24
+ const cacheKey = `${getAllVersions ? 'allVersions' : 'currentVersions'}-${returnBody ? 'withBody' : 'noBody'}`;
24
25
 
25
- // Check if we have cached domains for this specific getAllVersions value
26
- if (cachedServices[cacheKey].length > 0) {
27
- return cachedServices[cacheKey];
26
+ // Check if we have cached services
27
+ if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0 && CACHE_ENABLED) {
28
+ // console.timeEnd('✅ New getServices');
29
+ return memoryCache[cacheKey];
28
30
  }
29
31
 
30
- // Get services that are not versioned
31
- const services = await getCollection('services', (service) => {
32
- return (getAllVersions || !service.filePath?.includes('versioned')) && service.data.hidden !== true;
32
+ // 1. Fetch all collections in parallel
33
+ const [allServices, allEvents, allCommands, allQueries, allEntities, allContainers, allFlows] = await Promise.all([
34
+ getCollection('services'),
35
+ getCollection('events'),
36
+ getCollection('commands'),
37
+ getCollection('queries'),
38
+ getCollection('entities'),
39
+ getCollection('containers'),
40
+ getCollection('flows'),
41
+ ]);
42
+
43
+ const allMessages = [...allEvents, ...allCommands, ...allQueries];
44
+
45
+ // 2. Build optimized maps
46
+ const serviceMap = createVersionedMap(allServices);
47
+ const messageMap = createVersionedMap(allMessages);
48
+ const entityMap = createVersionedMap(allEntities);
49
+ const containerMap = createVersionedMap(allContainers);
50
+ const flowMap = createVersionedMap(allFlows);
51
+
52
+ // 3. Filter services
53
+ const targetServices = allServices.filter((service) => {
54
+ if (service.data.hidden === true) return false;
55
+ if (!getAllVersions && service.filePath?.includes('versioned')) return false;
56
+ return true;
33
57
  });
34
58
 
35
- const events = await getCollection('events');
36
- const commands = await getCollection('commands');
37
- const queries = await getCollection('queries');
38
- const entities = await getCollection('entities');
39
- const containers = await getCollection('containers');
40
- const allMessages = [...events, ...commands, ...queries];
59
+ const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? '');
60
+
61
+ // 4. Enrich services using Map lookups (O(1))
62
+ const processedServices = await Promise.all(
63
+ targetServices.map(async (service) => {
64
+ // Version info
65
+ const serviceVersions = serviceMap.get(service.data.id) || [];
66
+ const latestVersion = serviceVersions[0]?.data.version || service.data.version;
67
+ const versions = serviceVersions.map((s) => s.data.version);
68
+
69
+ const sends = (service.data.sends || [])
70
+ .map((m) => findInMap(messageMap, m.id, m.version))
71
+ .filter((e): e is CollectionEntry<CollectionMessageTypes> => !!e);
72
+
73
+ const receives = (service.data.receives || [])
74
+ .map((m) => findInMap(messageMap, m.id, m.version))
75
+ .filter((e): e is CollectionEntry<CollectionMessageTypes> => !!e);
76
+
77
+ const mappedEntities = (service.data.entities || [])
78
+ .map((e) => findInMap(entityMap, e.id, e.version))
79
+ .filter((e): e is CollectionEntry<'entities'> => !!e);
80
+
81
+ const mappedWritesTo = (service.data.writesTo || [])
82
+ .map((c) => findInMap(containerMap, c.id, c.version))
83
+ .filter((e): e is CollectionEntry<'containers'> => !!e);
84
+
85
+ const mappedReadsFrom = (service.data.readsFrom || [])
86
+ .map((c) => findInMap(containerMap, c.id, c.version))
87
+ .filter((e): e is CollectionEntry<'containers'> => !!e);
88
+
89
+ const mappedFlows = (service.data.flows || [])
90
+ .map((f) => findInMap(flowMap, f.id, f.version))
91
+ .filter((f): f is CollectionEntry<'flows'> => !!f);
41
92
 
42
- // @ts-ignore // TODO: Fix this type
43
- cachedServices[cacheKey] = await Promise.all(
44
- services.map(async (service) => {
45
- const { latestVersion, versions } = getVersionForCollectionItem(service, services);
46
-
47
- const sendsMessages = service.data.sends || [];
48
- const receivesMessages = service.data.receives || [];
49
- const serviceEntities = service.data.entities || [];
50
- const serviceWritesTo = service.data.writesTo || [];
51
- const serviceReadsFrom = service.data.readsFrom || [];
52
-
53
- const sends = sendsMessages
54
- .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
55
- .flat()
56
- .filter((e: any) => e !== undefined);
57
-
58
- const receives = receivesMessages
59
- .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
60
- .flat()
61
- .filter((e: any) => e !== undefined);
62
-
63
- const mappedEntities = serviceEntities
64
- .map((entity: any) => getItemsFromCollectionByIdAndSemverOrLatest(entities, entity.id, entity.version))
65
- .flat()
66
- .filter((e: any) => e !== undefined);
67
-
68
- const mappedWritesTo = serviceWritesTo
69
- .map((container: any) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version))
70
- .flat()
71
- .filter((e: any) => e !== undefined);
72
-
73
- const mappedReadsFrom = serviceReadsFrom
74
- .map((container: any) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version))
75
- .flat()
76
- .filter((e: any) => e !== undefined);
77
-
78
- const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? '');
79
93
  const folderName = await getResourceFolderName(
80
94
  process.env.PROJECT_DIR ?? '',
81
95
  service.data.id,
@@ -87,18 +101,19 @@ export const getServices = async ({ getAllVersions = true }: Props = {}): Promis
87
101
  ...service,
88
102
  data: {
89
103
  ...service.data,
90
- writesTo: mappedWritesTo,
91
- readsFrom: mappedReadsFrom,
92
- receives,
93
- sends,
104
+ writesTo: mappedWritesTo as any,
105
+ readsFrom: mappedReadsFrom as any,
106
+ flows: mappedFlows as any,
107
+ receives: receives as any,
108
+ sends: sends as any,
94
109
  versions,
95
110
  latestVersion,
96
- entities: mappedEntities,
111
+ entities: mappedEntities as any,
97
112
  },
98
113
  // TODO: verify if it could be deleted.
99
114
  nodes: {
100
- receives,
101
- sends,
115
+ receives: receives as any,
116
+ sends: sends as any,
102
117
  },
103
118
  catalog: {
104
119
  // TODO: avoid use string replace at path due to win32
@@ -110,16 +125,20 @@ export const getServices = async ({ getAllVersions = true }: Props = {}): Promis
110
125
  publicPath: path.join('/generated', service.collection, serviceFolderName),
111
126
  type: 'service',
112
127
  },
128
+ body: returnBody ? service.body : undefined,
113
129
  };
114
130
  })
115
131
  );
116
132
 
117
133
  // order them by the name of the service
118
- cachedServices[cacheKey].sort((a, b) => {
134
+ processedServices.sort((a, b) => {
119
135
  return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id);
120
136
  });
121
137
 
122
- return cachedServices[cacheKey];
138
+ memoryCache[cacheKey] = processedServices;
139
+ // console.timeEnd('✅ New getServices');
140
+
141
+ return processedServices;
123
142
  };
124
143
 
125
144
  export const getProducersOfMessage = (services: Service[], message: CollectionEntry<'events' | 'commands' | 'queries'>) => {
@@ -209,3 +228,16 @@ export const getProducersAndConsumersForChannel = async (channel: CollectionEntr
209
228
  consumers: consumers ?? [],
210
229
  };
211
230
  };
231
+ export const getServicesNotInAnyDomain = async (): Promise<Service[]> => {
232
+ const services = await getServices({ getAllVersions: false });
233
+
234
+ // We need an async-aware filter: run all lookups, then filter by the results
235
+ const domainCountsForServices = await Promise.all(
236
+ services.map(async (service) => {
237
+ const domainsForService = await getDomainsForService(service);
238
+ return domainsForService.length;
239
+ })
240
+ );
241
+
242
+ return services.filter((_, index) => domainCountsForServices[index] === 0);
243
+ };
@@ -0,0 +1,94 @@
1
+ import type { CollectionTypes } from '@types';
2
+ import { getCollection } from 'astro:content';
3
+ import type { CollectionEntry } from 'astro:content';
4
+ import path from 'path';
5
+
6
+ export type Team = CollectionEntry<'teams'>;
7
+ const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
8
+ // Cache for build time
9
+ let memoryCache: Team[] = [];
10
+
11
+ export const getTeams = async (): Promise<Team[]> => {
12
+ // console.time('✅ New getTeams');
13
+ if (memoryCache.length > 0 && CACHE_ENABLED) {
14
+ // console.timeEnd('✅ New getTeams');
15
+ return memoryCache;
16
+ }
17
+
18
+ // 1. Fetch all collections in parallel
19
+ const [allTeams, allDomains, allServices, allEvents, allCommands, allQueries] = await Promise.all([
20
+ getCollection('teams'),
21
+ getCollection('domains'),
22
+ getCollection('services'),
23
+ getCollection('events'),
24
+ getCollection('commands'),
25
+ getCollection('queries'),
26
+ ]);
27
+
28
+ // 2. Filter teams
29
+ const targetTeams = allTeams.filter((team) => team.data.hidden !== true);
30
+
31
+ // 3. Build Owner Index: Map<OwnerID, Item[]>
32
+ // This index groups all items (domains, services, etc.) by their owner IDs.
33
+ // This allows O(1) lookup to find all items owned by a specific team.
34
+ const ownershipMap = new Map<string, CollectionEntry<CollectionTypes>[]>();
35
+
36
+ const addToIndex = (items: CollectionEntry<CollectionTypes>[]) => {
37
+ for (const item of items) {
38
+ if (item.data.owners) {
39
+ for (const owner of item.data.owners) {
40
+ if (!ownershipMap.has(owner.id)) {
41
+ ownershipMap.set(owner.id, []);
42
+ }
43
+ ownershipMap.get(owner.id)!.push(item);
44
+ }
45
+ }
46
+ }
47
+ };
48
+
49
+ addToIndex(allDomains);
50
+ addToIndex(allServices);
51
+ addToIndex(allEvents);
52
+ addToIndex(allCommands);
53
+ addToIndex(allQueries);
54
+
55
+ // 4. Enrich teams using the ownership index
56
+ const processedTeams = targetTeams.map((team) => {
57
+ const teamId = team.data.id;
58
+ const ownedItems = ownershipMap.get(teamId) || [];
59
+
60
+ // Categorize items
61
+ const ownedDomains = ownedItems.filter((i) => i.collection === 'domains') as CollectionEntry<'domains'>[];
62
+ const ownedServices = ownedItems.filter((i) => i.collection === 'services') as CollectionEntry<'services'>[];
63
+ const ownedEvents = ownedItems.filter((i) => i.collection === 'events') as CollectionEntry<'events'>[];
64
+ const ownedCommands = ownedItems.filter((i) => i.collection === 'commands') as CollectionEntry<'commands'>[];
65
+ const ownedQueries = ownedItems.filter((i) => i.collection === 'queries') as CollectionEntry<'queries'>[];
66
+
67
+ return {
68
+ ...team,
69
+ data: {
70
+ ...team.data,
71
+ ownedDomains,
72
+ ownedServices,
73
+ ownedCommands,
74
+ ownedQueries,
75
+ ownedEvents,
76
+ },
77
+ catalog: {
78
+ path: path.join(team.collection, team.id.replace('/index.mdx', '')),
79
+ filePath: path.join(process.cwd(), 'src', 'catalog-files', team.collection, team.id.replace('/index.mdx', '')),
80
+ type: 'team',
81
+ },
82
+ };
83
+ });
84
+
85
+ // order them by the name of the team
86
+ processedTeams.sort((a, b) => {
87
+ return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id);
88
+ });
89
+
90
+ memoryCache = processedTeams;
91
+ // console.timeEnd('✅ New getTeams');
92
+
93
+ return processedTeams;
94
+ };
@@ -0,0 +1,122 @@
1
+ import type { CollectionTypes } from '@types';
2
+ import { getCollection } from 'astro:content';
3
+ import type { CollectionEntry } from 'astro:content';
4
+ import path from 'path';
5
+
6
+ export type User = CollectionEntry<'users'>;
7
+
8
+ // Simple in-memory cache
9
+ let memoryCache: User[] = [];
10
+
11
+ export const getUsers = async (): Promise<User[]> => {
12
+ // console.time('✅ New getUsers');
13
+
14
+ if (memoryCache.length > 0) {
15
+ // console.timeEnd('✅ New getUsers');
16
+ return memoryCache;
17
+ }
18
+
19
+ // 1. Fetch all collections in parallel
20
+ const [allUsers, allDomains, allServices, allEvents, allCommands, allQueries, allTeams] = await Promise.all([
21
+ getCollection('users'),
22
+ getCollection('domains'),
23
+ getCollection('services'),
24
+ getCollection('events'),
25
+ getCollection('commands'),
26
+ getCollection('queries'),
27
+ getCollection('teams'),
28
+ ]);
29
+
30
+ // 2. Filter users
31
+ const targetUsers = allUsers.filter((user) => user.data.hidden !== true);
32
+ const visibleTeams = allTeams.filter((team) => team.data.hidden !== true);
33
+
34
+ // 3. Process users (Optimization: Iterate once over relationships if possible,
35
+ // but since we need to check ownership for EACH user against ALL items,
36
+ // we can't easily invert the map without building an "owner" index first.
37
+ // Given users/teams count is usually lower than events/services, iterating users and filtering items is acceptable,
38
+ // OR we can index items by ownerID for O(1) lookup. Let's try indexing items by ownerID.)
39
+
40
+ // Build Owner Index: Map<OwnerID, Item[]>
41
+ const ownershipMap = new Map<string, CollectionEntry<CollectionTypes>[]>();
42
+
43
+ const addToIndex = (items: CollectionEntry<CollectionTypes>[]) => {
44
+ for (const item of items) {
45
+ if (item.data.owners) {
46
+ for (const owner of item.data.owners) {
47
+ if (!ownershipMap.has(owner.id)) {
48
+ ownershipMap.set(owner.id, []);
49
+ }
50
+ ownershipMap.get(owner.id)!.push(item);
51
+ }
52
+ }
53
+ }
54
+ };
55
+
56
+ addToIndex(allDomains);
57
+ addToIndex(allServices);
58
+ addToIndex(allEvents);
59
+ addToIndex(allCommands);
60
+ addToIndex(allQueries);
61
+
62
+ // Team Membership Index: Map<UserID, Team[]>
63
+ const teamMembershipMap = new Map<string, typeof visibleTeams>();
64
+ for (const team of visibleTeams) {
65
+ if (team.data.members) {
66
+ for (const member of team.data.members) {
67
+ if (!teamMembershipMap.has(member.id)) {
68
+ teamMembershipMap.set(member.id, []);
69
+ }
70
+ teamMembershipMap.get(member.id)!.push(team);
71
+ }
72
+ }
73
+ }
74
+
75
+ const mappedUsers = targetUsers.map((user) => {
76
+ const userId = user.data.id;
77
+ const associatedTeams = teamMembershipMap.get(userId) || [];
78
+ const associatedTeamIds = associatedTeams.map((t) => t.data.id);
79
+
80
+ // Collect all owned items directly owned by user OR by their teams
81
+ const directOwnedItems = ownershipMap.get(userId) || [];
82
+ const teamOwnedItems = associatedTeamIds.flatMap((teamId) => ownershipMap.get(teamId) || []);
83
+
84
+ // Combine and deduplicate items (by ID+Version or just reference equality since they come from same source arrays)
85
+ const allOwnedItems = Array.from(new Set([...directOwnedItems, ...teamOwnedItems]));
86
+
87
+ // Categorize items
88
+ const ownedDomains = allOwnedItems.filter((i) => i.collection === 'domains') as CollectionEntry<'domains'>[];
89
+ const ownedServices = allOwnedItems.filter((i) => i.collection === 'services') as CollectionEntry<'services'>[];
90
+ const ownedEvents = allOwnedItems.filter((i) => i.collection === 'events') as CollectionEntry<'events'>[];
91
+ const ownedCommands = allOwnedItems.filter((i) => i.collection === 'commands') as CollectionEntry<'commands'>[];
92
+ const ownedQueries = allOwnedItems.filter((i) => i.collection === 'queries') as CollectionEntry<'queries'>[];
93
+
94
+ return {
95
+ ...user,
96
+ data: {
97
+ ...user.data,
98
+ ownedDomains,
99
+ ownedServices,
100
+ ownedEvents,
101
+ ownedCommands,
102
+ ownedQueries,
103
+ associatedTeams,
104
+ },
105
+ catalog: {
106
+ path: path.join(user.collection, user.id.replace('/index.mdx', '')),
107
+ filePath: path.join(process.cwd(), 'src', 'catalog-files', user.collection, user.id.replace('/index.mdx', '')),
108
+ type: 'user',
109
+ },
110
+ };
111
+ });
112
+
113
+ // order them by the name of the user
114
+ mappedUsers.sort((a, b) => {
115
+ return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id);
116
+ });
117
+
118
+ memoryCache = mappedUsers;
119
+ // console.timeEnd('✅ New getUsers');
120
+
121
+ return mappedUsers;
122
+ };
@@ -1,6 +1,6 @@
1
1
  import type { CollectionTypes } from '@types';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import { coerce, compare, eq, satisfies as satisfiesRange } from 'semver';
3
+ import semver, { coerce, compare, eq, satisfies as satisfiesRange } from 'semver';
4
4
 
5
5
  export const getPreviousVersion = (version: string, versions: string[]) => {
6
6
  const index = versions.indexOf(version);
@@ -116,6 +116,7 @@ export const resourceToCollectionMap = {
116
116
  user: 'users',
117
117
  team: 'teams',
118
118
  container: 'containers',
119
+ entity: 'entities',
119
120
  } as const;
120
121
 
121
122
  export const collectionToResourceMap = {
@@ -129,6 +130,7 @@ export const collectionToResourceMap = {
129
130
  users: 'user',
130
131
  teams: 'team',
131
132
  containers: 'container',
133
+ entities: 'entity',
132
134
  } as const;
133
135
 
134
136
  export const getDeprecatedDetails = (item: CollectionEntry<CollectionTypes>) => {
@@ -165,3 +167,57 @@ export const removeContentFromCollection = (collection: CollectionEntry<Collecti
165
167
  catalog: undefined,
166
168
  }));
167
169
  };
170
+
171
+ // --- OPTIMIZATION HELPERS ---
172
+
173
+ /**
174
+ * Groups items by ID and sorts them by version (Newest first).
175
+ * This allows O(1) lookup for "latest" (index 0) and specific versions.
176
+ */
177
+ export const createVersionedMap = <T extends { data: { id: string; version?: string } }>(items: T[]) => {
178
+ const map = new Map<string, T[]>();
179
+
180
+ for (const item of items) {
181
+ const id = item.data.id;
182
+ if (!map.has(id)) map.set(id, []);
183
+ map.get(id)!.push(item);
184
+ }
185
+
186
+ // Sort every entry so index [0] is always the latest version
187
+ for (const [key, list] of map.entries()) {
188
+ list.sort((a, b) => {
189
+ // specific version sorting logic (fallback to string compare if not valid semver)
190
+ const vA = a.data.version || '0.0.0';
191
+ const vB = b.data.version || '0.0.0';
192
+ return semver.valid(vB) && semver.valid(vA) ? semver.rcompare(vA, vB) : vB.localeCompare(vA);
193
+ });
194
+ }
195
+ return map;
196
+ };
197
+
198
+ /**
199
+ * Fast lookup helper.
200
+ * If version is provided, find it. If not, return the first (latest) item.
201
+ */
202
+ export const findInMap = <T extends { data: { version?: string } }>(
203
+ map: Map<string, T[]>,
204
+ id: string,
205
+ version?: string
206
+ ): T | undefined => {
207
+ const items = map.get(id);
208
+ if (!items || items.length === 0) return undefined;
209
+
210
+ // If no version specified or 'latest', return the first item (which is sorted to be latest)
211
+ if (!version || version === 'latest') return items[0];
212
+
213
+ // Try exact match
214
+ const exactMatch = items.find((i) => i.data.version === version);
215
+ if (exactMatch) return exactMatch;
216
+
217
+ // Try semver match if not exact
218
+ if (semver.validRange(version)) {
219
+ return items.find((i) => semver.satisfies(i.data.version || '0.0.0', version));
220
+ }
221
+
222
+ return undefined;
223
+ };
@@ -34,7 +34,7 @@ export const showCustomBranding = () => {
34
34
  return isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled();
35
35
  };
36
36
 
37
- export const isChangelogEnabled = () => config?.changelog?.enabled ?? true;
37
+ export const isChangelogEnabled = () => config?.changelog?.enabled ?? false;
38
38
 
39
39
  export const isCustomDocsEnabled = () => isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled();
40
40
  export const isEventCatalogChatEnabled = () => {
@@ -55,5 +55,5 @@ export const isAuthEnabled = () => {
55
55
  };
56
56
 
57
57
  export const isSSR = () => config?.output === 'server';
58
-
58
+ export const isRSSEnabled = () => config?.rss?.enabled ?? false;
59
59
  export const isVisualiserEnabled = () => config?.visualiser?.enabled ?? true;
@@ -2,7 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { formatPatch, structuredPatch } from 'diff';
4
4
  import { html, parse } from 'diff2html';
5
- import { getItemsFromCollectionByIdAndSemverOrLatest } from './util';
5
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from './collections/util';
6
6
  import type { CollectionEntry } from 'astro:content';
7
7
  import type { CollectionTypes } from '@types';
8
8
 
@@ -17,12 +17,14 @@ interface Props {
17
17
  }
18
18
 
19
19
  export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simple', channelRenderMode = 'flat' }: Props) => {
20
+ // 1. Fetch data
20
21
  const containers = await getContainers();
21
22
 
22
23
  const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 });
23
24
  const nodes = [] as any,
24
25
  edges = [] as any;
25
26
 
27
+ // Optimized: Use find since we're looking for a specific item
26
28
  const container = containers.find((container) => container.data.id === id && container.data.version === version);
27
29
 
28
30
  // Nothing found...
@@ -2,9 +2,9 @@ import { getCollection, getEntry } from 'astro:content';
2
2
  import { generateIdForNode } from './utils/utils';
3
3
  import ELK from 'elkjs/lib/elk.bundled.js';
4
4
  import { MarkerType } from '@xyflow/react';
5
- import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
5
+ import { createVersionedMap, findInMap } from '@utils/collections/util';
6
6
  import { getVersionFromCollection } from '@utils/collections/versions';
7
- import { getEntities, type Entity } from '@utils/entities';
7
+ import { getEntities, type Entity } from '@utils/collections/entities';
8
8
  import { getDomains, type Domain } from '@utils/collections/domains';
9
9
  import { getServices, type Service } from '@utils/collections/services';
10
10
 
@@ -21,9 +21,8 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
21
21
  let nodes = [] as any,
22
22
  edges = [] as any;
23
23
 
24
- const allDomains = await getDomains();
25
- const allEntities = await getEntities();
26
- const allServices = await getServices();
24
+ // 1. Fetch all collections in parallel
25
+ const [allDomains, allEntities, allServices] = await Promise.all([getDomains(), getEntities(), getServices()]);
27
26
 
28
27
  let resource = null;
29
28
 
@@ -65,14 +64,25 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
65
64
  const externalToDomain = Array.from(new Set<string>(listOfReferencedEntities as string[])) // Remove duplicates
66
65
  .filter((entityId: any) => !resourceEntities.some((domainEntity: any) => domainEntity.id === entityId));
67
66
 
67
+ // 2. Build optimized maps
68
+ // Only build domain map if we have domains to search
69
+ // Only build entity map if we have entities to search
70
+ const entityMap = createVersionedMap(allEntities);
71
+
68
72
  // Helper function to find which domain an entity belongs to
73
+ // Optimized to use direct iteration over domains (domains usually contain entity arrays)
74
+ // We can't easily map entity->domain without scanning domains first unless we build a reverse index.
75
+ // Given domains count is usually manageable, scanning is acceptable, OR we could build an index if needed.
76
+ // For now, let's keep the scan but make it efficient.
69
77
  const findEntityDomain = (entityId: string) => {
70
78
  return allDomains.find((domain) => domain.data.entities?.some((domainEntity: any) => domainEntity.data.id === entityId));
71
79
  };
72
80
 
73
81
  const addedExternalEntities = [];
82
+
74
83
  for (const entityId of externalToDomain) {
75
- const externalEntity = getItemsFromCollectionByIdAndSemverOrLatest(allEntities, entityId as string, 'latest')[0] as Entity;
84
+ // 3. Use map lookup
85
+ const externalEntity = findInMap(entityMap, entityId as string, 'latest') as Entity;
76
86
 
77
87
  if (externalEntity) {
78
88
  const nodeId = generateIdForNode(externalEntity);
@@ -1,9 +1,10 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
2
  import dagre from 'dagre';
3
3
  import { generateIdForNode, createDagreGraph, calculatedNodes, createEdge } from '@utils/node-graphs/utils/utils';
4
- import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
4
+ import { findInMap, createVersionedMap } from '@utils/collections/util';
5
5
  import type { Node, Edge } from '@xyflow/react';
6
6
  import { getDomains } from '@utils/collections/domains';
7
+ import type { CollectionMessageTypes } from '@types';
7
8
 
8
9
  interface DomainCanvasData {
9
10
  domainNodes: Node[];
@@ -69,10 +70,14 @@ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
69
70
  } as Node);
70
71
  }
71
72
 
72
- // Get all messages for version resolution
73
- const allMessages = await getCollection('events')
74
- .then((events) => Promise.all([events, getCollection('commands'), getCollection('queries')]))
75
- .then(([events, commands, queries]) => [...events, ...commands, ...queries]);
73
+ // Get all messages for version resolution in parallel
74
+ const [events, commands, queries] = await Promise.all([
75
+ getCollection('events'),
76
+ getCollection('commands'),
77
+ getCollection('queries'),
78
+ ]);
79
+ const allMessages = [...events, ...commands, ...queries];
80
+ const messageMap = createVersionedMap(allMessages);
76
81
 
77
82
  // Map to track unique messages and their publishers/consumers across domains
78
83
  const messageRelationships = new Map<
@@ -89,9 +94,9 @@ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
89
94
  domainData.services.forEach((service: any) => {
90
95
  // Track messages this service sends
91
96
  const sendsRaw = service.data.sends ?? [];
97
+
92
98
  const sendsHydrated = sendsRaw
93
- .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
94
- .flat()
99
+ .map((message: any) => findInMap(messageMap, message.id, message.version))
95
100
  .filter((e: any) => e !== undefined);
96
101
 
97
102
  sendsHydrated.forEach((sentMessage: any) => {
@@ -115,8 +120,7 @@ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
115
120
  // Track messages this service receives
116
121
  const receivesRaw = service.data.receives ?? [];
117
122
  const receivesHydrated = receivesRaw
118
- .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
119
- .flat()
123
+ .map((message: any) => findInMap(messageMap, message.id, message.version))
120
124
  .filter((e: any) => e !== undefined);
121
125
 
122
126
  receivesHydrated.forEach((receivedMessage: any) => {
@@ -154,7 +158,7 @@ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
154
158
 
155
159
  if (crossesDomainBoundary) {
156
160
  // Find the actual message object
157
- const messageObject = allMessages.find((m) => m.data.id === message.id && m.data.version === message.version);
161
+ const messageObject = findInMap(messageMap, message.id, message.version) as CollectionEntry<CollectionMessageTypes>;
158
162
 
159
163
  if (messageObject) {
160
164
  const messageNodeId = `message-${messageKey}`;