@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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/count-resources.cjs +1 -0
- package/dist/analytics/count-resources.js +1 -1
- package/dist/analytics/log-build.cjs +3 -1
- package/dist/analytics/log-build.js +4 -4
- package/dist/{chunk-VQLDZRHC.js → chunk-2EI3M7OO.js} +1 -1
- package/dist/{chunk-COPXPOV2.js → chunk-7M5IQL3J.js} +1 -1
- package/dist/{chunk-3DVHEVHQ.js → chunk-DAOXTQVS.js} +1 -0
- package/dist/{chunk-OH2U6UEJ.js → chunk-KY74BE42.js} +1 -1
- package/dist/{chunk-LYRAK5LI.js → chunk-QV2PKXZM.js} +3 -2
- package/dist/{chunk-QMORF42U.js → chunk-ZONBICNH.js} +8 -0
- package/dist/{chunk-YWG7CCN7.js → chunk-ZQHBDPIY.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/developer-tools/api-catalog.md +114 -0
- package/dist/eventcatalog.cjs +11 -1
- package/dist/eventcatalog.js +7 -7
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/search-indexer.cjs +8 -0
- package/dist/search-indexer.js +1 -1
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/Attachments.astro +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +11 -2
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +100 -2
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +53 -1
- package/eventcatalog/src/content.config.ts +61 -0
- package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +19 -0
- package/eventcatalog/src/layouts/DiscoverLayout.astro +12 -1
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +98 -46
- package/eventcatalog/src/pages/.well-known/api-catalog.ts +191 -0
- package/eventcatalog/src/pages/api-catalog/specifications/[collection]/[id]/[version]/[specification].ts +109 -0
- package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -0
- package/eventcatalog/src/pages/discover/[type]/index.astro +17 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +68 -2
- package/eventcatalog/src/pages/docs/llm/llms-full.txt.ts +1 -0
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +26 -1
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +26 -1
- package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +150 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +50 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +209 -68
- package/eventcatalog/src/types/index.ts +2 -0
- package/eventcatalog/src/utils/collection-colors.ts +2 -0
- package/eventcatalog/src/utils/collections/adr-constants.ts +53 -0
- package/eventcatalog/src/utils/collections/adrs.ts +146 -0
- package/eventcatalog/src/utils/collections/icons.ts +2 -0
- package/eventcatalog/src/utils/collections/teams.ts +6 -1
- package/eventcatalog/src/utils/collections/users.ts +17 -10
- package/eventcatalog/src/utils/collections/util.ts +2 -0
- package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
- 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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
340
|
-
|
|
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) ?? [],
|