@eventcatalog/core 5.7.5 → 5.8.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/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-NNR5RP47.js → chunk-DT63JMNP.js} +1 -1
- package/dist/{chunk-SAB3WVWU.js → chunk-E636DOFG.js} +1 -1
- package/dist/{chunk-3I4MO4G6.js → chunk-WLXXZTLD.js} +1 -1
- package/dist/{chunk-SR6NZ4QL.js → chunk-YU6FEGOK.js} +1 -1
- package/dist/{chunk-Q6ZCTZM3.js → chunk-Z6UUCTIB.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/components/components/15-nodegraph.md +2 -2
- package/dist/docs/development/guides/resources/agents/03-adding-tools.md +6 -0
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/SpecificationDownloadButton.astro +24 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +70 -70
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
- package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +25 -27
- package/eventcatalog/src/pages/discover/[type]/index.astro +9 -2
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -7
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename].astro +21 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +19 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +4 -0
- package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/services/[id]/spec/[filename]/[file].ts +7 -0
- package/eventcatalog/src/pages/docs/services/_latest-raw-specification-route.ts +37 -0
- package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +7 -3
- package/eventcatalog/src/utils/raw-specification.ts +126 -0
- package/package.json +4 -4
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
|
-
import { getCollection } from 'astro:content';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs';
|
|
2
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
5
3
|
import { getSpecificationsForService } from '@utils/collections/services';
|
|
4
|
+
import { getSpecificationMediaType } from '@utils/collections/util';
|
|
6
5
|
import { resourceFileExists, readResourceFile } from '@utils/resource-files';
|
|
7
6
|
|
|
7
|
+
// The Schema API: kept for the API consumers that already use it. It finds a specification by its type, so it
|
|
8
|
+
// serves the first of each type. The file of every specification is also served next to its page:
|
|
9
|
+
// /docs/{type}/{id}/{version}/{asyncapi|spec|graphql|generic}/{filename}/{file}
|
|
10
|
+
const getFirstSpecificationOfEachType = (service: CollectionEntry<'services'>) =>
|
|
11
|
+
getSpecificationsForService(service).filter(
|
|
12
|
+
(specification, index, specifications) => specifications.findIndex((spec) => spec.type === specification.type) === index
|
|
13
|
+
);
|
|
14
|
+
|
|
8
15
|
export async function getStaticPaths() {
|
|
9
16
|
const services = await getCollection('services');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
specification: specification.type,
|
|
24
|
-
},
|
|
25
|
-
props: {
|
|
26
|
-
schema: fs.readFileSync(path.join(path.dirname(service.filePath ?? ''), specification.path ?? ''), 'utf8'),
|
|
27
|
-
},
|
|
28
|
-
})),
|
|
29
|
-
];
|
|
30
|
-
},
|
|
31
|
-
[] as { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[]
|
|
17
|
+
return services.flatMap((service) =>
|
|
18
|
+
getFirstSpecificationOfEachType(service).map((specification) => ({
|
|
19
|
+
params: {
|
|
20
|
+
collection: service.collection,
|
|
21
|
+
id: service.data.id,
|
|
22
|
+
version: service.data.version,
|
|
23
|
+
specification: specification.type,
|
|
24
|
+
},
|
|
25
|
+
props: {
|
|
26
|
+
schema: readResourceFile(service, specification.path),
|
|
27
|
+
contentType: getSpecificationMediaType(specification),
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
32
30
|
);
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -36,7 +34,7 @@ export const GET: APIRoute = async ({ props, params }) => {
|
|
|
36
34
|
// In static mode, props are pre-computed by getStaticPaths
|
|
37
35
|
if (props.schema) {
|
|
38
36
|
return new Response(props.schema, {
|
|
39
|
-
headers: { 'Content-Type':
|
|
37
|
+
headers: { 'Content-Type': props.contentType },
|
|
40
38
|
});
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -80,6 +78,6 @@ export const GET: APIRoute = async ({ props, params }) => {
|
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
return new Response(schema, {
|
|
83
|
-
headers: { 'Content-Type':
|
|
81
|
+
headers: { 'Content-Type': getSpecificationMediaType(spec) },
|
|
84
82
|
});
|
|
85
83
|
};
|
|
@@ -282,6 +282,13 @@ function resolvePointer(pointer: { id: string; version?: string }) {
|
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
// Resolve a list of pointers, listing each resource once (a data product can point at the
|
|
286
|
+
// same resource more than once, e.g. one output per data contract)
|
|
287
|
+
function resolvePointers(pointers: { id: string; version?: string }[] = []) {
|
|
288
|
+
const resolved = pointers.map(resolvePointer).filter((item) => item !== null);
|
|
289
|
+
return [...new Map(resolved.map((item) => [`${item.collection}:${item.data.id}:${item.data.version}`, item])).values()];
|
|
290
|
+
}
|
|
291
|
+
|
|
285
292
|
// Build a Set of subdomain IDs (domains that are nested within other domains)
|
|
286
293
|
const allSubdomainIds = new Set(
|
|
287
294
|
domains.flatMap((d: any) => (d.data?.domains || []).map((sd: any) => sd.data?.id || sd.id)).filter(Boolean)
|
|
@@ -368,8 +375,8 @@ const tableData = enrichedData.map((d: any) => ({
|
|
|
368
375
|
flows: d.data?.flows?.map(mapToItem) ?? [],
|
|
369
376
|
servicesThatWriteToContainer: d.data?.servicesThatWriteToContainer?.map(mapToItem) ?? [],
|
|
370
377
|
servicesThatReadFromContainer: d.data?.servicesThatReadFromContainer?.map(mapToItem) ?? [],
|
|
371
|
-
inputs: d.data?.inputs
|
|
372
|
-
outputs: d.data?.outputs
|
|
378
|
+
inputs: resolvePointers(d.data?.inputs),
|
|
379
|
+
outputs: resolvePointers(d.data?.outputs),
|
|
373
380
|
},
|
|
374
381
|
}));
|
|
375
382
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { createRawSpecificationRoute } from '@utils/raw-specification';
|
|
3
|
+
import { Page } from '../_[filename].data';
|
|
4
|
+
|
|
5
|
+
// The specification's file, as written, next to its page
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createRawSpecificationRoute('asyncapi', Page);
|
|
@@ -12,6 +12,8 @@ import js from '@asyncapi/react-component/browser/standalone/without-parser.js?u
|
|
|
12
12
|
import { AsyncApiComponentWP, type ConfigInterface } from '@asyncapi/react-component';
|
|
13
13
|
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
14
14
|
import CopyAsMarkdown from '@components/CopyAsMarkdown';
|
|
15
|
+
import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
|
|
16
|
+
import { getRawSpecificationUrl } from '@utils/raw-specification';
|
|
15
17
|
import { isEventCatalogChatVisible } from '@utils/feature';
|
|
16
18
|
import Config from '@utils/eventcatalog-config/catalog';
|
|
17
19
|
import { Page } from './_[filename].data';
|
|
@@ -23,7 +25,7 @@ export const getStaticPaths = Page.getStaticPaths;
|
|
|
23
25
|
// Get data
|
|
24
26
|
const pageData = await Page.getData(Astro);
|
|
25
27
|
if (!pageData) return Astro.rewrite('/404');
|
|
26
|
-
const { collection, data, filePath, filename, path: relativeSpecPath, headers = {} } = pageData;
|
|
28
|
+
const { collection, data, filePath, filename, filenameWithoutExtension, path: relativeSpecPath, headers = {} } = pageData;
|
|
27
29
|
|
|
28
30
|
const fileName = filename || 'asyncapi.yaml';
|
|
29
31
|
const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
|
|
@@ -149,6 +151,15 @@ const renderedComponent = renderToString(component);
|
|
|
149
151
|
// Capitalize the first letter of a string
|
|
150
152
|
const pageTitle = `${collection} | ${data.name} | AsyncApi Spec`.replace(/^\w/, (c) => c.toUpperCase());
|
|
151
153
|
|
|
154
|
+
const downloadUrl = getRawSpecificationUrl({
|
|
155
|
+
collection,
|
|
156
|
+
id: data.id,
|
|
157
|
+
version: data.version,
|
|
158
|
+
type: 'asyncapi',
|
|
159
|
+
filename,
|
|
160
|
+
filenameWithoutExtension,
|
|
161
|
+
});
|
|
162
|
+
|
|
152
163
|
// Chat configuration
|
|
153
164
|
const chatEnabled = isEventCatalogChatVisible();
|
|
154
165
|
const chatQuery = `Tell me about the AsyncAPI specification for "${data.name}" (version ${data.version})`;
|
|
@@ -175,9 +186,10 @@ const pagefindAttributes =
|
|
|
175
186
|
// https://github.com/CloudCannon/pagefind/issues/437
|
|
176
187
|
}
|
|
177
188
|
<h2 class="hidden">{pageTitle}</h2>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
<div class="absolute top-3 right-4 z-10 flex items-center gap-2" data-pagefind-ignore>
|
|
190
|
+
{(fileExists || isRemote) && <SpecificationDownloadButton href={downloadUrl} filename={filename} />}
|
|
191
|
+
{
|
|
192
|
+
chatEnabled && (
|
|
181
193
|
<CopyAsMarkdown
|
|
182
194
|
client:only="react"
|
|
183
195
|
schemas={[]}
|
|
@@ -189,9 +201,9 @@ const pagefindAttributes =
|
|
|
189
201
|
preferChatAsDefault={true}
|
|
190
202
|
chatButtonText="Ask about this specification"
|
|
191
203
|
/>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
</div>
|
|
195
207
|
{
|
|
196
208
|
failedToParse ? (
|
|
197
209
|
<div class="not-prose mx-auto my-12 max-w-3xl rounded-lg border border-red-300 bg-[rgb(var(--ec-card-bg))] p-6">
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { createRawSpecificationRoute } from '@utils/raw-specification';
|
|
3
|
+
import { Page } from '../_[filename].data';
|
|
4
|
+
|
|
5
|
+
// The specification's file, as written, next to its page
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createRawSpecificationRoute('generic', Page);
|
|
@@ -10,6 +10,8 @@ import { resolveHeaders } from '@utils/remote-spec';
|
|
|
10
10
|
import Admonition from '@components/MDX/Admonition';
|
|
11
11
|
import NewVersionBanner from '@components/NewVersionBanner.astro';
|
|
12
12
|
import Badge from '@components/Badge.astro';
|
|
13
|
+
import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
|
|
14
|
+
import { getRawSpecificationUrl } from '@utils/raw-specification';
|
|
13
15
|
import { getResourceTypeBadge } from '@utils/collections/resource-type-badge';
|
|
14
16
|
import { getResourceNoun, getSpecificationCodeLanguage } from '@utils/collections/util';
|
|
15
17
|
|
|
@@ -49,6 +51,15 @@ if (isRemote) {
|
|
|
49
51
|
content = readResourceFile(pageData, relativeSpecPath) ?? '';
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
const downloadUrl = getRawSpecificationUrl({
|
|
55
|
+
collection,
|
|
56
|
+
id: data.id,
|
|
57
|
+
version: data.version,
|
|
58
|
+
type: 'generic',
|
|
59
|
+
filename,
|
|
60
|
+
filenameWithoutExtension,
|
|
61
|
+
});
|
|
62
|
+
|
|
52
63
|
const iconURL = resolveIconUrl(getGenericSpecificationIcon(icon));
|
|
53
64
|
|
|
54
65
|
// Create comprehensive page title
|
|
@@ -80,7 +91,16 @@ const pagefindAttributes =
|
|
|
80
91
|
<div class="w-full min-w-0 lg:mr-2 xl:pr-12 py-8 bg-[rgb(var(--ec-page-bg))]">
|
|
81
92
|
<!-- Title and subtitle sized like the resource's overview page, kept compact so the source starts high up -->
|
|
82
93
|
<div class="border-b border-[rgb(var(--ec-page-border))] pb-4">
|
|
83
|
-
<
|
|
94
|
+
<div class="flex items-start justify-between gap-4">
|
|
95
|
+
<h1 class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name}</h1>
|
|
96
|
+
{
|
|
97
|
+
fileExists && (
|
|
98
|
+
<div data-pagefind-ignore>
|
|
99
|
+
<SpecificationDownloadButton href={downloadUrl} filename={filename} />
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
</div>
|
|
84
104
|
<p class="text-base pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{specificationName}</p>
|
|
85
105
|
<div class="flex flex-wrap gap-1.5 pt-3">
|
|
86
106
|
{badges.map((badge: any) => <Badge badge={badge} />)}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { createRawSpecificationRoute } from '@utils/raw-specification';
|
|
3
|
+
import { Page } from '../_[filename].data';
|
|
4
|
+
|
|
5
|
+
// The specification's file, as written, next to its page
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createRawSpecificationRoute('graphql', Page);
|
|
@@ -10,6 +10,8 @@ import { buildUrl, buildEditUrlForResource } from '@utils/url-builder';
|
|
|
10
10
|
import Admonition from '@components/MDX/Admonition';
|
|
11
11
|
import NewVersionBanner from '@components/NewVersionBanner.astro';
|
|
12
12
|
import Badge from '@components/Badge.astro';
|
|
13
|
+
import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
|
|
14
|
+
import { getRawSpecificationUrl } from '@utils/raw-specification';
|
|
13
15
|
import { getResourceTypeBadge } from '@utils/collections/resource-type-badge';
|
|
14
16
|
import { getResourceNoun } from '@utils/collections/util';
|
|
15
17
|
|
|
@@ -19,7 +21,7 @@ export const getStaticPaths = Page.getStaticPaths;
|
|
|
19
21
|
// Get data
|
|
20
22
|
const pageData = await Page.getData(Astro);
|
|
21
23
|
if (!pageData) return Astro.rewrite('/404');
|
|
22
|
-
const { collection, data, filePath, filename, path: relativeSpecPath } = pageData;
|
|
24
|
+
const { collection, data, filePath, filename, filenameWithoutExtension, path: relativeSpecPath } = pageData;
|
|
23
25
|
|
|
24
26
|
const fileName = filename || 'schema.graphql';
|
|
25
27
|
const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
|
|
@@ -36,6 +38,15 @@ if (isRemote) {
|
|
|
36
38
|
content = await fetch(relativeSpecPath).then((res) => res.text());
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
const downloadUrl = getRawSpecificationUrl({
|
|
42
|
+
collection,
|
|
43
|
+
id: data.id,
|
|
44
|
+
version: data.version,
|
|
45
|
+
type: 'graphql',
|
|
46
|
+
filename,
|
|
47
|
+
filenameWithoutExtension,
|
|
48
|
+
});
|
|
49
|
+
|
|
39
50
|
// Create comprehensive page title
|
|
40
51
|
const pageTitle = `${collection} | ${data.name} | GraphQL Schema`.replace(/^\w/, (c) => c.toUpperCase());
|
|
41
52
|
|
|
@@ -79,6 +90,13 @@ const pagefindAttributes =
|
|
|
79
90
|
</h1>
|
|
80
91
|
<h2 class="text-lg text-[rgb(var(--ec-page-text-muted))] font-medium mb-1">GraphQL Schema</h2>
|
|
81
92
|
</div>
|
|
93
|
+
{
|
|
94
|
+
(fileExists || isRemote) && (
|
|
95
|
+
<div data-pagefind-ignore>
|
|
96
|
+
<SpecificationDownloadButton href={downloadUrl} filename={filename} />
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
82
100
|
</div>
|
|
83
101
|
|
|
84
102
|
{
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { createRawSpecificationRoute } from '@utils/raw-specification';
|
|
3
|
+
import { Page } from '../_[filename].data';
|
|
4
|
+
|
|
5
|
+
// The specification's file, as written, next to its page
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createRawSpecificationRoute('openapi', Page);
|
|
@@ -76,6 +76,10 @@ if (isRemote) {
|
|
|
76
76
|
// https://github.com/CloudCannon/pagefind/issues/437
|
|
77
77
|
}
|
|
78
78
|
<h2 class="hidden">{pageTitle}</h2>
|
|
79
|
+
{
|
|
80
|
+
// No download button here, unlike the other specification pages: Scalar shows its own
|
|
81
|
+
// "Download OpenAPI Document" link. The file is also served at ./{filename}/{file}.
|
|
82
|
+
}
|
|
79
83
|
{chatEnabled && (
|
|
80
84
|
<div class="absolute top-3 right-14 z-10">
|
|
81
85
|
<CopyAsMarkdown
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { Page } from '../../../../[type]/[id]/[version]/asyncapi/_[filename].data';
|
|
3
|
+
import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
|
|
4
|
+
|
|
5
|
+
// The file of the service's latest specification, at a URL that doesn't change with each release
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('asyncapi', Page);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { Page } from '../../../../[type]/[id]/[version]/graphql/_[filename].data';
|
|
3
|
+
import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
|
|
4
|
+
|
|
5
|
+
// The file of the service's latest specification, at a URL that doesn't change with each release
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('graphql', Page);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isSSR } from '@utils/feature';
|
|
2
|
+
import { Page } from '../../../../[type]/[id]/[version]/spec/_[filename].data';
|
|
3
|
+
import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
|
|
4
|
+
|
|
5
|
+
// The file of the service's latest specification, at a URL that doesn't change with each release
|
|
6
|
+
export const prerender = !isSSR();
|
|
7
|
+
export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('openapi', Page);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIContext, APIRoute } from 'astro';
|
|
2
|
+
import type { SpecificationType } from '@utils/collections/util';
|
|
3
|
+
import {
|
|
4
|
+
getRawSpecificationResponse,
|
|
5
|
+
isRawSpecificationFile,
|
|
6
|
+
notFound,
|
|
7
|
+
toRawSpecificationPaths,
|
|
8
|
+
type SpecificationPage,
|
|
9
|
+
type SpecificationPageData,
|
|
10
|
+
type StaticPath,
|
|
11
|
+
} from '@utils/raw-specification';
|
|
12
|
+
import { getLatestServicePaths, getLatestServiceVersion } from './_latest-version-route';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The route that serves the file of a service's latest version, for links that don't change with each
|
|
16
|
+
* release: /docs/services/{id}/{asyncapi|spec|graphql}/{filename}/{file}
|
|
17
|
+
*/
|
|
18
|
+
export const createLatestServiceRawSpecificationRoute = (type: SpecificationType, Page: SpecificationPage) => {
|
|
19
|
+
const getStaticPaths = async () =>
|
|
20
|
+
(await getLatestServicePaths(toRawSpecificationPaths(await Page.getStaticPaths()))) as StaticPath[];
|
|
21
|
+
|
|
22
|
+
const GET: APIRoute = async ({ params, props }: APIContext) => {
|
|
23
|
+
const version = props.redirectVersion ?? (params.id ? await getLatestServiceVersion(params.id) : undefined);
|
|
24
|
+
if (!version) return notFound('Specification not found');
|
|
25
|
+
|
|
26
|
+
const pageData: SpecificationPageData | undefined = await Page.getData({
|
|
27
|
+
params: { type: 'services', id: params.id, version, filename: params.filename },
|
|
28
|
+
props,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!pageData || !isRawSpecificationFile(pageData, params.file)) return notFound('Specification not found');
|
|
32
|
+
|
|
33
|
+
return getRawSpecificationResponse(type, pageData);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return { getStaticPaths, GET };
|
|
37
|
+
};
|
|
@@ -119,9 +119,13 @@ export const buildDataProductSections = (
|
|
|
119
119
|
context.resourceDocCategories
|
|
120
120
|
);
|
|
121
121
|
|
|
122
|
-
// Resolve inputs and outputs to their proper sidebar references
|
|
123
|
-
|
|
124
|
-
const
|
|
122
|
+
// Resolve inputs and outputs to their proper sidebar references. The same resource can be
|
|
123
|
+
// listed more than once (e.g. one output per data contract), so show each resource only once.
|
|
124
|
+
const resolveRefs = (pointers: { id: string; version?: string }[]) => [
|
|
125
|
+
...new Set(pointers.map((pointer) => resolvePointerToRef(pointer, context)).filter((ref): ref is string => !!ref)),
|
|
126
|
+
];
|
|
127
|
+
const resolvedInputs = resolveRefs(inputs);
|
|
128
|
+
const resolvedOutputs = resolveRefs(outputs);
|
|
125
129
|
|
|
126
130
|
// Extract data contracts from outputs that have a contract field
|
|
127
131
|
const dataContracts = outputs
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { APIContext, APIRoute } from 'astro';
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import type { CollectionTypes } from '@types';
|
|
4
|
+
import { getSpecificationMediaType, processSpecifications, type SpecificationType } from '@utils/collections/util';
|
|
5
|
+
import { readResourceFile, resourceFileExists } from '@utils/resource-files';
|
|
6
|
+
import { resolveHeaders } from '@utils/remote-spec';
|
|
7
|
+
import { buildUrl } from '@utils/url-builder';
|
|
8
|
+
import Config from '@utils/eventcatalog-config/catalog';
|
|
9
|
+
|
|
10
|
+
/** The folder each kind of specification page lives in, under /docs/{type}/{id}/{version}/ */
|
|
11
|
+
const PAGE_SEGMENT_BY_SPECIFICATION_TYPE: Record<SpecificationType, string> = {
|
|
12
|
+
asyncapi: 'asyncapi',
|
|
13
|
+
openapi: 'spec',
|
|
14
|
+
graphql: 'graphql',
|
|
15
|
+
generic: 'generic',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type StaticPath = { params: Record<string, string | undefined>; props: Record<string, any> };
|
|
19
|
+
|
|
20
|
+
/** The data a specification page loads: the resource, plus the specification's path and file names */
|
|
21
|
+
export type SpecificationPageData = CollectionEntry<CollectionTypes> & {
|
|
22
|
+
path: string;
|
|
23
|
+
filename: string;
|
|
24
|
+
filenameWithoutExtension: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A specification page's data loader (the `Page` class in each `_[filename].data.ts`) */
|
|
28
|
+
export type SpecificationPage = {
|
|
29
|
+
getStaticPaths(): Promise<StaticPath[]>;
|
|
30
|
+
getData(astro: { params: Record<string, string | undefined>; props: Record<string, any> }): Promise<any>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The name a specification's file has in its URL: a remote specification's file name without the URL's query
|
|
35
|
+
* or fragment (orders.yaml?token=abc is orders.yaml), which would otherwise end the URL's path
|
|
36
|
+
*/
|
|
37
|
+
export const getRawSpecificationFileName = (filename: string) => filename.split(/[?#]/, 1)[0];
|
|
38
|
+
|
|
39
|
+
/** Whether a raw specification URL's `file` names this specification's file */
|
|
40
|
+
export const isRawSpecificationFile = (pageData: SpecificationPageData, file: string | undefined) =>
|
|
41
|
+
getRawSpecificationFileName(pageData.filename) === file;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The URL of a specification's file, as written (JSON, YAML, GraphQL...): its page's URL followed by the
|
|
45
|
+
* file's name, e.g. /docs/services/Orders/1.0.0/asyncapi/orders/orders.yaml
|
|
46
|
+
*/
|
|
47
|
+
export const getRawSpecificationUrl = ({
|
|
48
|
+
collection,
|
|
49
|
+
id,
|
|
50
|
+
version,
|
|
51
|
+
type,
|
|
52
|
+
filename,
|
|
53
|
+
filenameWithoutExtension,
|
|
54
|
+
}: {
|
|
55
|
+
collection: string;
|
|
56
|
+
id: string;
|
|
57
|
+
version: string;
|
|
58
|
+
type: SpecificationType;
|
|
59
|
+
filename: string;
|
|
60
|
+
filenameWithoutExtension: string;
|
|
61
|
+
}) =>
|
|
62
|
+
buildUrl(
|
|
63
|
+
`/docs/${collection}/${id}/${version}/${PAGE_SEGMENT_BY_SPECIFICATION_TYPE[type]}/${filenameWithoutExtension}/${getRawSpecificationFileName(filename)}`,
|
|
64
|
+
true
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const isRemote = (path: string) => /^https?:\/\//i.test(path);
|
|
68
|
+
|
|
69
|
+
export const notFound = (error: string) =>
|
|
70
|
+
new Response(JSON.stringify({ error }), { status: 404, headers: { 'Content-Type': 'application/json' } });
|
|
71
|
+
|
|
72
|
+
/** The specification's file, as written, with the media type of its format */
|
|
73
|
+
export const getRawSpecificationResponse = async (
|
|
74
|
+
type: SpecificationType,
|
|
75
|
+
pageData: SpecificationPageData
|
|
76
|
+
): Promise<Response> => {
|
|
77
|
+
const specification = processSpecifications((pageData.data as any).specifications).find(
|
|
78
|
+
(spec) => spec.type === type && spec.path === pageData.path
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!specification) return notFound('Specification not found');
|
|
82
|
+
|
|
83
|
+
const headers = { 'Content-Type': getSpecificationMediaType(specification) };
|
|
84
|
+
|
|
85
|
+
if (isRemote(specification.path)) {
|
|
86
|
+
const allowAnyEnvInHeaders = type === 'asyncapi' && (Config?.asyncAPI?.allowAnyEnvInSpecHeaders ?? false);
|
|
87
|
+
const response = await fetch(specification.path, {
|
|
88
|
+
headers: resolveHeaders(specification.headers ?? {}, { allowAnyEnvInHeaders }),
|
|
89
|
+
}).catch(() => null);
|
|
90
|
+
|
|
91
|
+
if (!response?.ok) return notFound('Specification file could not be fetched');
|
|
92
|
+
return new Response(await response.text(), { headers });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = readResourceFile(pageData, specification.path);
|
|
96
|
+
if (content === null) return notFound('Specification file could not be read');
|
|
97
|
+
|
|
98
|
+
return new Response(content, { headers });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Each specification page's path, with the specification's file name added as `file`. A local file that
|
|
103
|
+
* doesn't exist gets no path: a static build would otherwise save the 404's JSON under the file's name.
|
|
104
|
+
*/
|
|
105
|
+
export const toRawSpecificationPaths = (paths: StaticPath[]): StaticPath[] =>
|
|
106
|
+
paths
|
|
107
|
+
.filter(({ props }) => isRemote(props.path) || resourceFileExists(props, props.path))
|
|
108
|
+
.map(({ params, props }) => ({ params: { ...params, file: getRawSpecificationFileName(props.filename) }, props }));
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The route that serves a specification's file next to its page:
|
|
112
|
+
* /docs/{type}/{id}/{version}/{asyncapi|spec|graphql|generic}/{filename}/{file}
|
|
113
|
+
*/
|
|
114
|
+
export const createRawSpecificationRoute = (type: SpecificationType, Page: SpecificationPage) => {
|
|
115
|
+
const getStaticPaths = async () => toRawSpecificationPaths(await Page.getStaticPaths());
|
|
116
|
+
|
|
117
|
+
const GET: APIRoute = async ({ params, props }: APIContext) => {
|
|
118
|
+
const pageData: SpecificationPageData | undefined = await Page.getData({ params, props });
|
|
119
|
+
|
|
120
|
+
if (!pageData || !isRawSpecificationFile(pageData, params.file)) return notFound('Specification not found');
|
|
121
|
+
|
|
122
|
+
return getRawSpecificationResponse(type, pageData);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return { getStaticPaths, GET };
|
|
126
|
+
};
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "BUSL-1.1",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.8.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -141,11 +141,11 @@
|
|
|
141
141
|
"y-protocols": "^1.0.7",
|
|
142
142
|
"yjs": "^13.6.33",
|
|
143
143
|
"zod": "^4.6.6",
|
|
144
|
-
"@eventcatalog/fitness": "0.1.1",
|
|
145
144
|
"@eventcatalog/exporters": "0.1.4",
|
|
146
|
-
"@eventcatalog/
|
|
145
|
+
"@eventcatalog/fitness": "0.1.1",
|
|
147
146
|
"@eventcatalog/sdk": "2.32.0",
|
|
148
|
-
"@eventcatalog/visualiser": "^5.2.
|
|
147
|
+
"@eventcatalog/visualiser": "^5.2.4",
|
|
148
|
+
"@eventcatalog/linter": "1.3.1"
|
|
149
149
|
},
|
|
150
150
|
"devDependencies": {
|
|
151
151
|
"@astrojs/check": "^0.9.10",
|