@eventcatalog/core 3.41.4 → 3.43.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 (55) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/count-resources.cjs +1 -0
  4. package/dist/analytics/count-resources.js +1 -1
  5. package/dist/analytics/log-build.cjs +3 -1
  6. package/dist/analytics/log-build.js +4 -4
  7. package/dist/{chunk-VQLDZRHC.js → chunk-2EI3M7OO.js} +1 -1
  8. package/dist/{chunk-COPXPOV2.js → chunk-7M5IQL3J.js} +1 -1
  9. package/dist/{chunk-3DVHEVHQ.js → chunk-DAOXTQVS.js} +1 -0
  10. package/dist/{chunk-OH2U6UEJ.js → chunk-KY74BE42.js} +1 -1
  11. package/dist/{chunk-LYRAK5LI.js → chunk-QV2PKXZM.js} +3 -2
  12. package/dist/{chunk-QMORF42U.js → chunk-ZONBICNH.js} +8 -0
  13. package/dist/{chunk-YWG7CCN7.js → chunk-ZQHBDPIY.js} +1 -1
  14. package/dist/constants.cjs +1 -1
  15. package/dist/constants.js +1 -1
  16. package/dist/docs/development/developer-tools/api-catalog.md +114 -0
  17. package/dist/eventcatalog.cjs +11 -1
  18. package/dist/eventcatalog.js +7 -7
  19. package/dist/generate.cjs +1 -1
  20. package/dist/generate.js +3 -3
  21. package/dist/search-indexer.cjs +8 -0
  22. package/dist/search-indexer.js +1 -1
  23. package/dist/utils/cli-logger.cjs +1 -1
  24. package/dist/utils/cli-logger.js +2 -2
  25. package/eventcatalog/src/components/MDX/Attachments.astro +3 -3
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +11 -2
  27. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +100 -2
  28. package/eventcatalog/src/components/Tables/Discover/columns.tsx +53 -1
  29. package/eventcatalog/src/content.config.ts +61 -0
  30. package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +19 -0
  31. package/eventcatalog/src/layouts/DiscoverLayout.astro +12 -1
  32. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +98 -46
  33. package/eventcatalog/src/pages/.well-known/api-catalog.ts +191 -0
  34. package/eventcatalog/src/pages/api-catalog/specifications/[collection]/[id]/[version]/[specification].ts +109 -0
  35. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -0
  36. package/eventcatalog/src/pages/discover/[type]/index.astro +17 -0
  37. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
  38. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +68 -2
  39. package/eventcatalog/src/pages/docs/llm/llms-full.txt.ts +1 -0
  40. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +26 -1
  41. package/eventcatalog/src/pages/docs/users/[id]/index.astro +26 -1
  42. package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +150 -0
  43. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +2 -0
  44. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +50 -0
  45. package/eventcatalog/src/stores/sidebar-store/state.ts +209 -68
  46. package/eventcatalog/src/types/index.ts +2 -0
  47. package/eventcatalog/src/utils/collection-colors.ts +2 -0
  48. package/eventcatalog/src/utils/collections/adr-constants.ts +53 -0
  49. package/eventcatalog/src/utils/collections/adrs.ts +146 -0
  50. package/eventcatalog/src/utils/collections/icons.ts +2 -0
  51. package/eventcatalog/src/utils/collections/teams.ts +6 -1
  52. package/eventcatalog/src/utils/collections/users.ts +17 -10
  53. package/eventcatalog/src/utils/collections/util.ts +2 -0
  54. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  55. package/package.json +1 -1
@@ -11,6 +11,7 @@ import {
11
11
  TableProperties,
12
12
  BotMessageSquare,
13
13
  BookOpen,
14
+ BookText,
14
15
  FileText,
15
16
  SquareDashedMousePointerIcon,
16
17
  FileCode,
@@ -61,15 +62,16 @@ import { getEvents } from '@utils/collections/events';
61
62
  import { getServices } from '@utils/collections/services';
62
63
  import { getAgents } from '@utils/collections/agents';
63
64
  import { getFlows } from '@utils/collections/flows';
65
+ import { getAdrs } from '@utils/collections/adrs';
66
+ import { getContainers } from '@utils/collections/containers';
67
+ import { getDataProducts } from '@utils/collections/data-products';
64
68
  import { isCollectionVisibleInCatalog } from '@eventcatalog';
65
69
  import { buildUrl } from '@utils/url-builder';
66
70
  import { getQueries } from '@utils/collections/queries';
67
- import { hasLandingPageForDocs } from '@utils/pages';
68
71
  import { filterSidebarItems } from '@utils/sidebar-visibility';
69
72
 
70
73
  import { isEmbedEnabled, isCustomStylesEnabled, isEventCatalogScaleEnabled, isCustomDocsEnabled, isSSR } from '@utils/feature';
71
74
 
72
- const catalogHasDefaultLandingPageForDocs = await hasLandingPageForDocs();
73
75
  const customDocs = await getCollection('customPages');
74
76
 
75
77
  let events: any[] = [];
@@ -77,20 +79,24 @@ let commands: any[] = [];
77
79
  let queries: any[] = [];
78
80
  let services: any[] = [];
79
81
  let agents: any[] = [];
82
+ let adrs: any[] = [];
80
83
  let domains: any[] = [];
81
84
  let flows: any[] = [];
82
-
83
- if (!catalogHasDefaultLandingPageForDocs) {
84
- [events, commands, queries, services, agents, domains, flows] = await Promise.all([
85
- getEvents({ getAllVersions: false, hydrateServices: false }),
86
- getCommands({ getAllVersions: false, hydrateServices: false }),
87
- getQueries({ getAllVersions: false, hydrateServices: false }),
88
- getServices({ getAllVersions: false }),
89
- getAgents({ getAllVersions: false }),
90
- getDomains({ getAllVersions: false }),
91
- getFlows({ getAllVersions: false }),
92
- ]);
93
- }
85
+ let containers: any[] = [];
86
+ let dataProducts: any[] = [];
87
+
88
+ [events, commands, queries, services, agents, adrs, domains, flows, containers, dataProducts] = await Promise.all([
89
+ getEvents({ getAllVersions: false, hydrateServices: false }),
90
+ getCommands({ getAllVersions: false, hydrateServices: false }),
91
+ getQueries({ getAllVersions: false, hydrateServices: false }),
92
+ getServices({ getAllVersions: false }),
93
+ getAgents({ getAllVersions: false }),
94
+ getAdrs({ getAllVersions: false }),
95
+ getDomains({ getAllVersions: false }),
96
+ getFlows({ getAllVersions: false }),
97
+ getContainers({ getAllVersions: false }),
98
+ getDataProducts({ getAllVersions: false }),
99
+ ]);
94
100
 
95
101
  // Try and load any custom styles if they exist
96
102
  if (isCustomStylesEnabled()) {
@@ -203,8 +209,47 @@ const premiumFeatures: Array<{
203
209
  isPremium?: boolean;
204
210
  }> = [];
205
211
 
212
+ const internalServices = services.filter((service) => !service.data.externalSystem);
213
+ const externalSystems = services.filter((service) => service.data.externalSystem);
214
+
206
215
  const browseItems = filterSidebarItems(
207
216
  [
217
+ {
218
+ id: '/discover/agents',
219
+ aliases: ['/discover'],
220
+ label: 'Agents',
221
+ icon: BotMessageSquare,
222
+ href: buildUrl('/discover/agents'),
223
+ current: currentPath === buildUrl('/discover/agents'),
224
+ visible: agents.length > 0,
225
+ },
226
+ {
227
+ id: '/discover/adrs',
228
+ aliases: ['/discover'],
229
+ label: 'Decision Records',
230
+ icon: BookText,
231
+ href: buildUrl('/discover/adrs'),
232
+ current: currentPath === buildUrl('/discover/adrs'),
233
+ visible: adrs.length > 0,
234
+ },
235
+ {
236
+ id: '/discover/data-products',
237
+ aliases: ['/discover'],
238
+ label: 'Data Products',
239
+ icon: CubeIcon,
240
+ href: buildUrl('/discover/data-products'),
241
+ current: currentPath === buildUrl('/discover/data-products'),
242
+ visible: dataProducts.length > 0,
243
+ },
244
+ {
245
+ id: '/discover/containers',
246
+ aliases: ['/discover'],
247
+ label: 'Data Stores',
248
+ icon: Database,
249
+ href: buildUrl('/discover/containers'),
250
+ current: currentPath === buildUrl('/discover/containers'),
251
+ visible: containers.length > 0,
252
+ },
208
253
  {
209
254
  id: '/discover/domains',
210
255
  aliases: ['/discover'],
@@ -212,6 +257,7 @@ const browseItems = filterSidebarItems(
212
257
  icon: RectangleGroupIcon,
213
258
  href: buildUrl('/discover/domains'),
214
259
  current: currentPath === buildUrl('/discover/domains'),
260
+ visible: domains.length > 0,
215
261
  },
216
262
  {
217
263
  id: '/discover/services',
@@ -220,6 +266,7 @@ const browseItems = filterSidebarItems(
220
266
  icon: ServerIcon,
221
267
  href: buildUrl('/discover/services'),
222
268
  current: currentPath === buildUrl('/discover/services'),
269
+ visible: internalServices.length > 0,
223
270
  },
224
271
  {
225
272
  id: '/discover/external-systems',
@@ -228,6 +275,7 @@ const browseItems = filterSidebarItems(
228
275
  icon: GlobeAltIcon,
229
276
  href: buildUrl('/discover/external-systems'),
230
277
  current: currentPath === buildUrl('/discover/external-systems'),
278
+ visible: externalSystems.length > 0,
231
279
  },
232
280
  {
233
281
  id: '/discover/events',
@@ -236,6 +284,7 @@ const browseItems = filterSidebarItems(
236
284
  icon: BoltIcon,
237
285
  href: buildUrl('/discover/events'),
238
286
  current: currentPath === buildUrl('/discover/events'),
287
+ visible: events.length > 0,
239
288
  },
240
289
  {
241
290
  id: '/discover/commands',
@@ -244,6 +293,7 @@ const browseItems = filterSidebarItems(
244
293
  icon: ChatBubbleLeftIcon,
245
294
  href: buildUrl('/discover/commands'),
246
295
  current: currentPath === buildUrl('/discover/commands'),
296
+ visible: commands.length > 0,
247
297
  },
248
298
  {
249
299
  id: '/discover/queries',
@@ -252,6 +302,7 @@ const browseItems = filterSidebarItems(
252
302
  icon: MagnifyingGlassIcon,
253
303
  href: buildUrl('/discover/queries'),
254
304
  current: currentPath === buildUrl('/discover/queries'),
305
+ visible: queries.length > 0,
255
306
  },
256
307
  {
257
308
  id: '/discover/flows',
@@ -260,34 +311,13 @@ const browseItems = filterSidebarItems(
260
311
  icon: QueueListIcon,
261
312
  href: buildUrl('/discover/flows'),
262
313
  current: currentPath === buildUrl('/discover/flows'),
263
- },
264
- {
265
- id: '/discover/containers',
266
- aliases: ['/discover'],
267
- label: 'Data Stores',
268
- icon: Database,
269
- href: buildUrl('/discover/containers'),
270
- current: currentPath === buildUrl('/discover/containers'),
271
- },
272
- {
273
- id: '/discover/data-products',
274
- aliases: ['/discover'],
275
- label: 'Data Products',
276
- icon: CubeIcon,
277
- href: buildUrl('/discover/data-products'),
278
- current: currentPath === buildUrl('/discover/data-products'),
279
- },
280
- {
281
- id: '/discover/agents',
282
- aliases: ['/discover'],
283
- label: 'Agents',
284
- icon: BotMessageSquare,
285
- href: buildUrl('/discover/agents'),
286
- current: currentPath === buildUrl('/discover/agents'),
314
+ visible: flows.length > 0,
287
315
  },
288
316
  ],
289
317
  userSideBarConfiguration
290
- );
318
+ )
319
+ .filter((item) => item.visible !== false)
320
+ .sort((a, b) => a.label.localeCompare(b.label));
291
321
 
292
322
  const organizationItems = filterSidebarItems(
293
323
  [
@@ -329,15 +359,25 @@ const currentNavigationItem = [...navigationItems, ...studioNavigationItem, ...p
329
359
  const { title, description, showNestedSideBar = true, showHeader = true } = Astro.props;
330
360
 
331
361
  const canPageBeEmbedded = isEmbedEnabled();
362
+ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/docs', true)];
332
363
  ---
333
364
 
334
365
  <BaseLayout title={`EventCatalog | ${title}`} description={description} ogTitle={title}>
335
366
  <Fragment slot="head">
336
- <script is:inline>
367
+ <script is:inline define:vars={{ verticalNavAutoCollapsePaths }}>
337
368
  (() => {
369
+ const verticalNavStorageKey = 'eventcatalog-vertical-nav-collapsed';
370
+ const routeShouldAutoCollapseVerticalNav = (pathname) =>
371
+ verticalNavAutoCollapsePaths.some((path) => pathname === path || pathname.startsWith(`${path}/`));
372
+
338
373
  try {
339
- const savedState = localStorage.getItem('eventcatalog-vertical-nav-collapsed');
340
- const isCollapsed = savedState === null ? true : savedState === 'true';
374
+ const shouldAutoCollapse = routeShouldAutoCollapseVerticalNav(window.location.pathname);
375
+ if (shouldAutoCollapse) {
376
+ localStorage.setItem(verticalNavStorageKey, 'true');
377
+ }
378
+
379
+ const savedState = localStorage.getItem(verticalNavStorageKey);
380
+ const isCollapsed = shouldAutoCollapse ? true : savedState === null ? true : savedState === 'true';
341
381
  document.documentElement.setAttribute('data-vertical-nav-collapsed', isCollapsed ? 'true' : 'false');
342
382
  } catch (error) {
343
383
  document.documentElement.setAttribute('data-vertical-nav-collapsed', 'true');
@@ -737,6 +777,7 @@ const canPageBeEmbedded = isEmbedEnabled();
737
777
  navigationItems,
738
778
  currentNavigationItem,
739
779
  canPageBeEmbedded,
780
+ verticalNavAutoCollapsePaths,
740
781
  }}
741
782
  >
742
783
  const VERTICAL_NAV_STORAGE_KEY = 'eventcatalog-vertical-nav-collapsed';
@@ -817,7 +858,21 @@ const canPageBeEmbedded = isEmbedEnabled();
817
858
  });
818
859
  };
819
860
 
861
+ const routeShouldAutoCollapseVerticalNav = (pathname) =>
862
+ verticalNavAutoCollapsePaths.some((path) => pathname === path || pathname.startsWith(`${path}/`));
863
+
864
+ const persistVerticalNavCollapsedState = (collapsed) => {
865
+ try {
866
+ localStorage.setItem(VERTICAL_NAV_STORAGE_KEY, String(collapsed));
867
+ } catch (error) {}
868
+ };
869
+
820
870
  const getPersistedVerticalNavCollapsedState = () => {
871
+ if (routeShouldAutoCollapseVerticalNav(window.location.pathname)) {
872
+ persistVerticalNavCollapsedState(true);
873
+ return true;
874
+ }
875
+
821
876
  try {
822
877
  const savedState = localStorage.getItem(VERTICAL_NAV_STORAGE_KEY);
823
878
  return savedState === null ? true : savedState === 'true';
@@ -948,10 +1003,7 @@ const canPageBeEmbedded = isEmbedEnabled();
948
1003
  const nextState = !isCollapsed;
949
1004
 
950
1005
  setVerticalNavCollapsedState(nextState);
951
-
952
- try {
953
- localStorage.setItem(VERTICAL_NAV_STORAGE_KEY, String(nextState));
954
- } catch (error) {}
1006
+ persistVerticalNavCollapsedState(nextState);
955
1007
  };
956
1008
  }
957
1009
  });
@@ -0,0 +1,191 @@
1
+ import type { APIRoute } from 'astro';
2
+ import yaml from 'js-yaml';
3
+ import { getServices, getSpecificationsForService } from '@utils/collections/services';
4
+ import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
5
+ import type { ProcessedSpecification } from '@utils/collections/util';
6
+ import { buildUrl } from '@utils/url-builder';
7
+ import { readResourceFile } from '@utils/resource-files';
8
+ import { isEventCatalogMCPEnabled } from '@utils/feature';
9
+
10
+ const RFC_9727_PROFILE = 'https://www.rfc-editor.org/info/rfc9727';
11
+ const LINKSET_CONTENT_TYPE = `application/linkset+json; profile="${RFC_9727_PROFILE}"`;
12
+
13
+ type LinkTarget = {
14
+ href: string;
15
+ type?: string;
16
+ title?: string;
17
+ };
18
+
19
+ type ApiCatalogEntry = {
20
+ anchor: string;
21
+ 'service-desc': LinkTarget[];
22
+ 'service-doc'?: LinkTarget[];
23
+ };
24
+
25
+ type ApiCatalogResource = Awaited<ReturnType<typeof getServices>>[number] | Awaited<ReturnType<typeof getDomains>>[number];
26
+
27
+ const absoluteUrl = (request: Request, pathOrUrl: string) => new URL(pathOrUrl, request.url).toString();
28
+
29
+ const getSpecificationMediaType = (specification: ProcessedSpecification) => {
30
+ const extension = specification.filename.split('.').pop()?.toLowerCase();
31
+
32
+ if (specification.type === 'graphql') return 'application/graphql';
33
+ if (extension === 'json') return 'application/json';
34
+ if (extension === 'yaml' || extension === 'yml') return 'application/yaml';
35
+
36
+ return 'text/plain';
37
+ };
38
+
39
+ const getSpecificationIdentifier = (specification: ProcessedSpecification) => {
40
+ return `${specification.type}-${Buffer.from(specification.path).toString('base64url')}`;
41
+ };
42
+
43
+ const parseSpecification = (rawSpecification: string, path: string): unknown => {
44
+ if (path.endsWith('.json')) {
45
+ return JSON.parse(rawSpecification);
46
+ }
47
+
48
+ return yaml.load(rawSpecification);
49
+ };
50
+
51
+ const toHttpUrl = (value: unknown): string | undefined => {
52
+ if (typeof value !== 'string' || value.trim() === '') return undefined;
53
+ if (!/^https?:\/\//i.test(value)) return undefined;
54
+
55
+ try {
56
+ const url = new URL(value);
57
+ if (url.protocol === 'http:' || url.protocol === 'https:') {
58
+ return url.toString();
59
+ }
60
+ } catch {
61
+ return undefined;
62
+ }
63
+
64
+ return undefined;
65
+ };
66
+
67
+ const getSpecificationsForResource = (resource: ApiCatalogResource) => {
68
+ if (resource.collection === 'domains') {
69
+ return getSpecificationsForDomain(resource);
70
+ }
71
+
72
+ return getSpecificationsForService(resource);
73
+ };
74
+
75
+ const getEndpointFromSpecification = (request: Request, resource: ApiCatalogResource) => {
76
+ const specifications = getSpecificationsForResource(resource);
77
+
78
+ for (const specification of specifications) {
79
+ if (specification.type !== 'openapi' && specification.type !== 'asyncapi') continue;
80
+
81
+ const rawSpecification = readResourceFile(resource, specification.path);
82
+ if (!rawSpecification) continue;
83
+
84
+ try {
85
+ const parsedSpecification = parseSpecification(rawSpecification, specification.path) as any;
86
+
87
+ if (specification.type === 'openapi') {
88
+ const serverUrl = parsedSpecification?.servers?.find((server: any) => typeof server?.url === 'string')?.url;
89
+ const endpoint = toHttpUrl(serverUrl);
90
+ if (endpoint) return endpoint;
91
+ }
92
+
93
+ if (specification.type === 'asyncapi') {
94
+ const servers = Object.values(parsedSpecification?.servers ?? {}) as any[];
95
+ const serverUrl = servers.find((server) => typeof server?.url === 'string')?.url;
96
+ const endpoint = toHttpUrl(serverUrl);
97
+ if (endpoint) return endpoint;
98
+ }
99
+ } catch {
100
+ // Invalid or unsupported specifications should not prevent catalog discovery.
101
+ }
102
+ }
103
+ };
104
+
105
+ const getResourceDocumentationUrl = (request: Request, resource: ApiCatalogResource) => {
106
+ return absoluteUrl(request, buildUrl(`/docs/${resource.collection}/${resource.data.id}/${resource.data.version}`, true));
107
+ };
108
+
109
+ const getResourceMarkdownUrl = (request: Request, resource: ApiCatalogResource) => {
110
+ return absoluteUrl(request, buildUrl(`/docs/${resource.collection}/${resource.data.id}/${resource.data.version}.md`, true));
111
+ };
112
+
113
+ const toApiCatalogEntry = (request: Request, resource: ApiCatalogResource): ApiCatalogEntry | null => {
114
+ const specifications = getSpecificationsForResource(resource);
115
+ if (specifications.length === 0) return null;
116
+
117
+ const resourceDocumentationUrl = getResourceDocumentationUrl(request, resource);
118
+ const resourceMarkdownUrl = getResourceMarkdownUrl(request, resource);
119
+
120
+ return {
121
+ anchor: getEndpointFromSpecification(request, resource) ?? resourceDocumentationUrl,
122
+ 'service-desc': specifications.map((specification) => ({
123
+ href: absoluteUrl(
124
+ request,
125
+ buildUrl(
126
+ `/api-catalog/specifications/${resource.collection}/${resource.data.id}/${resource.data.version}/${getSpecificationIdentifier(specification)}`,
127
+ true
128
+ )
129
+ ),
130
+ type: getSpecificationMediaType(specification),
131
+ title: `${resource.data.name || resource.data.id} ${specification.name}`,
132
+ })),
133
+ 'service-doc': [
134
+ {
135
+ href: resourceMarkdownUrl,
136
+ type: 'text/markdown',
137
+ title: `${resource.data.name || resource.data.id} documentation`,
138
+ },
139
+ {
140
+ href: resourceDocumentationUrl,
141
+ type: 'text/html',
142
+ title: `${resource.data.name || resource.data.id} documentation`,
143
+ },
144
+ ],
145
+ };
146
+ };
147
+
148
+ const getMcpCatalogEntry = (request: Request): ApiCatalogEntry | null => {
149
+ if (!isEventCatalogMCPEnabled()) return null;
150
+
151
+ const mcpUrl = absoluteUrl(request, buildUrl('/docs/mcp', true));
152
+
153
+ return {
154
+ anchor: mcpUrl,
155
+ 'service-desc': [
156
+ {
157
+ href: mcpUrl,
158
+ type: 'application/json',
159
+ title: 'EventCatalog MCP Server',
160
+ },
161
+ ],
162
+ };
163
+ };
164
+
165
+ export const GET: APIRoute = async ({ request }) => {
166
+ const [services, domains] = await Promise.all([getServices({ getAllVersions: true }), getDomains({ getAllVersions: true })]);
167
+ const resources = [...services, ...domains];
168
+ const linkset = resources
169
+ .map((resource) => toApiCatalogEntry(request, resource))
170
+ .filter((entry): entry is ApiCatalogEntry => entry !== null);
171
+
172
+ const mcpEntry = getMcpCatalogEntry(request);
173
+ if (mcpEntry) {
174
+ linkset.push(mcpEntry);
175
+ }
176
+
177
+ return new Response(JSON.stringify({ linkset }, null, 2), {
178
+ headers: {
179
+ 'Content-Type': LINKSET_CONTENT_TYPE,
180
+ },
181
+ });
182
+ };
183
+
184
+ export const HEAD: APIRoute = async ({ request }) => {
185
+ return new Response(null, {
186
+ headers: {
187
+ 'Content-Type': LINKSET_CONTENT_TYPE,
188
+ Link: `<${absoluteUrl(request, buildUrl('/.well-known/api-catalog', true))}>; rel="api-catalog"`,
189
+ },
190
+ });
191
+ };
@@ -0,0 +1,109 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getCollection } from 'astro:content';
3
+ import { getSpecificationsForDomain } from '@utils/collections/domains';
4
+ import { getSpecificationsForService } from '@utils/collections/services';
5
+ import type { ProcessedSpecification } from '@utils/collections/util';
6
+ import { readResourceFile } from '@utils/resource-files';
7
+
8
+ type SupportedCollection = 'domains' | 'services';
9
+
10
+ const isSupportedCollection = (collection: string | undefined): collection is SupportedCollection => {
11
+ return collection === 'domains' || collection === 'services';
12
+ };
13
+
14
+ const getSpecificationMediaType = (specification: ProcessedSpecification) => {
15
+ const extension = specification.filename.split('.').pop()?.toLowerCase();
16
+
17
+ if (specification.type === 'graphql') return 'application/graphql';
18
+ if (extension === 'json') return 'application/json';
19
+ if (extension === 'yaml' || extension === 'yml') return 'application/yaml';
20
+
21
+ return 'text/plain';
22
+ };
23
+
24
+ const getSpecificationIdentifier = (specification: ProcessedSpecification) => {
25
+ return `${specification.type}-${Buffer.from(specification.path).toString('base64url')}`;
26
+ };
27
+
28
+ const getSpecificationsForResource = (
29
+ resource:
30
+ | Awaited<ReturnType<typeof getCollection<'services'>>>[number]
31
+ | Awaited<ReturnType<typeof getCollection<'domains'>>>[number]
32
+ ) => {
33
+ if (resource.collection === 'domains') {
34
+ return getSpecificationsForDomain(resource);
35
+ }
36
+
37
+ return getSpecificationsForService(resource);
38
+ };
39
+
40
+ export async function getStaticPaths() {
41
+ const [services, domains] = await Promise.all([getCollection('services'), getCollection('domains')]);
42
+ const resources = [...services, ...domains].filter((resource) => resource.data.hidden !== true);
43
+
44
+ return resources.flatMap((resource) =>
45
+ getSpecificationsForResource(resource).map((specification) => ({
46
+ params: {
47
+ collection: resource.collection,
48
+ id: resource.data.id,
49
+ version: resource.data.version,
50
+ specification: getSpecificationIdentifier(specification),
51
+ },
52
+ props: {
53
+ rawSpecification: readResourceFile(resource, specification.path),
54
+ contentType: getSpecificationMediaType(specification),
55
+ },
56
+ }))
57
+ );
58
+ }
59
+
60
+ export const GET: APIRoute = async ({ params, props }) => {
61
+ if (props.rawSpecification) {
62
+ return new Response(props.rawSpecification, {
63
+ headers: { 'Content-Type': props.contentType ?? 'text/plain' },
64
+ });
65
+ }
66
+
67
+ const { collection, id, version, specification } = params;
68
+
69
+ if (!isSupportedCollection(collection) || !id || !version || !specification) {
70
+ return new Response(JSON.stringify({ error: 'Missing or invalid collection, id, version, or specification parameter' }), {
71
+ status: 400,
72
+ headers: { 'Content-Type': 'application/json' },
73
+ });
74
+ }
75
+
76
+ const resources = await getCollection(collection);
77
+ const resource = resources.find((item) => item.data.id === id && item.data.version === version && item.data.hidden !== true);
78
+
79
+ if (!resource) {
80
+ return new Response(JSON.stringify({ error: 'Resource not found' }), {
81
+ status: 404,
82
+ headers: { 'Content-Type': 'application/json' },
83
+ });
84
+ }
85
+
86
+ const spec = getSpecificationsForResource(resource).find(
87
+ (item) => getSpecificationIdentifier(item) === specification || item.type === specification
88
+ );
89
+
90
+ if (!spec) {
91
+ return new Response(JSON.stringify({ error: 'Specification not found' }), {
92
+ status: 404,
93
+ headers: { 'Content-Type': 'application/json' },
94
+ });
95
+ }
96
+
97
+ const rawSpecification = readResourceFile(resource, spec.path);
98
+
99
+ if (!rawSpecification) {
100
+ return new Response(JSON.stringify({ error: 'Specification file could not be read' }), {
101
+ status: 404,
102
+ headers: { 'Content-Type': 'application/json' },
103
+ });
104
+ }
105
+
106
+ return new Response(rawSpecification, {
107
+ headers: { 'Content-Type': getSpecificationMediaType(spec) },
108
+ });
109
+ };
@@ -11,6 +11,7 @@ export class Page extends HybridPage {
11
11
  static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
12
12
  const { getFlows } = await import('@utils/collections/flows');
13
13
  const { getAgents } = await import('@utils/collections/agents');
14
+ const { getAdrs } = await import('@utils/collections/adrs');
14
15
  const { getServices } = await import('@utils/collections/services');
15
16
  const { getDataProducts } = await import('@utils/collections/data-products');
16
17
 
@@ -20,6 +21,7 @@ export class Page extends HybridPage {
20
21
  const loaders = {
21
22
  ...pageDataLoader,
22
23
  agents: getAgents,
24
+ adrs: getAdrs,
23
25
  flows: getFlows,
24
26
  services: getInternalServices,
25
27
  'external-systems': getExternalServices,
@@ -29,6 +31,7 @@ export class Page extends HybridPage {
29
31
  const itemTypes = [
30
32
  'events',
31
33
  'agents',
34
+ 'adrs',
32
35
  'commands',
33
36
  'queries',
34
37
  'domains',
@@ -60,6 +63,7 @@ export class Page extends HybridPage {
60
63
 
61
64
  const { getFlows } = await import('@utils/collections/flows');
62
65
  const { getAgents } = await import('@utils/collections/agents');
66
+ const { getAdrs } = await import('@utils/collections/adrs');
63
67
  const { getServices } = await import('@utils/collections/services');
64
68
  const { getDataProducts } = await import('@utils/collections/data-products');
65
69
 
@@ -69,6 +73,7 @@ export class Page extends HybridPage {
69
73
  const loaders = {
70
74
  ...pageDataLoader,
71
75
  agents: getAgents,
76
+ adrs: getAdrs,
72
77
  flows: getFlows,
73
78
  services: getInternalServices,
74
79
  'external-systems': getExternalServices,
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import { getCommands } from '@utils/collections/commands';
3
3
  import { getAgents } from '@utils/collections/agents';
4
+ import { createAdrStatusBadge, getAdrs } from '@utils/collections/adrs';
4
5
  import { getDomains, getDomainsForAgent, getDomainsForService } from '@utils/collections/domains';
5
6
  import { getEvents } from '@utils/collections/events';
6
7
  import { getServices } from '@utils/collections/services';
@@ -24,6 +25,7 @@ const events = await getEvents();
24
25
  const queries = await getQueries();
25
26
  const commands = await getCommands();
26
27
  const agents = await getAgents();
28
+ const adrs = await getAdrs();
27
29
  const services = await getServices();
28
30
  const domains = await getDomains({ getAllVersions: false });
29
31
  const containers = await getContainers();
@@ -108,6 +110,15 @@ const typeConfig: Record<
108
110
  { id: 'isDeprecated', label: 'Is Deprecated' },
109
111
  ],
110
112
  },
113
+ adrs: {
114
+ label: 'Decision Records',
115
+ description: 'Browse the decision records across your catalog and inspect their status and scope.',
116
+ propertyOptions: [
117
+ { id: 'hasOwners', label: 'Has Owners' },
118
+ { id: 'hasAppliesTo', label: 'Applies To Resources' },
119
+ { id: 'hasDecisionMakers', label: 'Has Decision Makers' },
120
+ ],
121
+ },
111
122
  'external-systems': {
112
123
  label: 'External Systems',
113
124
  description: 'Explore the external systems connected to your architecture and the contracts around them.',
@@ -200,6 +211,7 @@ function hasSpecifications(service: any): boolean {
200
211
  // Build lookup maps for all collections (for resolving data product inputs/outputs)
201
212
  const allCollections = [
202
213
  ...agents.map((a) => ({ ...a, collection: 'agents' })),
214
+ ...adrs.map((adr) => ({ ...adr, collection: 'adrs' })),
203
215
  ...services.map((s) => ({ ...s, collection: 'services' })),
204
216
  ...containers.map((c) => ({ ...c, collection: 'containers' })),
205
217
  ...channels.map((c) => ({ ...c, collection: 'channels' })),
@@ -266,6 +278,8 @@ const tableData = enrichedData.map((d: any) => ({
266
278
  hasDataDependencies: isServiceOrAgentLike ? (d.data?.writesTo || []).length > 0 || (d.data?.readsFrom || []).length > 0 : false,
267
279
  hasModel: type === 'agents' ? !!d.data?.model : false,
268
280
  hasTools: type === 'agents' ? (d.data?.tools || []).length > 0 : false,
281
+ hasAppliesTo: type === 'adrs' ? (d.data?.appliesTo || []).length > 0 : false,
282
+ hasDecisionMakers: type === 'adrs' ? (d.data?.decisionMakers || []).length > 0 : false,
269
283
  // Data-product-specific properties
270
284
  hasInputs: type === 'data-products' ? (d.data?.inputs || []).length > 0 : false,
271
285
  hasOutputs: type === 'data-products' ? (d.data?.outputs || []).length > 0 : false,
@@ -279,6 +293,9 @@ const tableData = enrichedData.map((d: any) => ({
279
293
  latestVersion: d.data?.latestVersion,
280
294
  draft: d.data?.draft,
281
295
  badges: d.data?.badges,
296
+ status: d.data?.status,
297
+ date: d.data?.date,
298
+ statusBadge: d.data?.status ? createAdrStatusBadge(d.data.status) : undefined,
282
299
  producers: d.data?.producers?.map(mapToItem) ?? [],
283
300
  consumers: d.data?.consumers?.map(mapToItem) ?? [],
284
301
  receives: d.data?.receives?.map(mapToItem) ?? [],
@@ -14,6 +14,7 @@ export class Page extends HybridPage {
14
14
 
15
15
  const itemTypes: PageTypes[] = [
16
16
  'agents',
17
+ 'adrs',
17
18
  'events',
18
19
  'commands',
19
20
  'queries',