@lukoweb/apitogo 0.2.50 → 0.2.52
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/cli/cli.js +128 -72
- package/dist/declarations/lib/plugins/openapi/util/createHttpSnippet.d.ts +2 -0
- package/package.json +1 -1
- package/src/config/v2/full-server.ts +1 -1
- package/src/config/v2/gateway-reference.ts +15 -2
- package/src/config/v2/init.ts +1 -1
- package/src/config/v2/starter-landing.ts +13 -6
- package/src/config/v2/wrapper.ts +22 -2
- package/src/lib/plugins/openapi/util/createHttpSnippet.ts +147 -135
- package/src/vite/api/SchemaManager.ts +376 -371
- package/src/vite/plugin-api.ts +328 -328
- package/src/vite/plugin-config.ts +192 -131
|
@@ -18,7 +18,13 @@ export type ReferenceOptions = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/** Header of the API key when the gateway does not say otherwise (same as the platform's OpenAPI export). */
|
|
21
|
-
export const DEFAULT_KEY_HEADER = "
|
|
21
|
+
export const DEFAULT_KEY_HEADER = "X-API-Key";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Operation extension naming the header a protected operation's key goes in. The API page's code samples read it
|
|
25
|
+
* (`createHttpSnippet`), so they show the header with a placeholder key.
|
|
26
|
+
*/
|
|
27
|
+
export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
|
|
22
28
|
|
|
23
29
|
const METHODS = [
|
|
24
30
|
"get",
|
|
@@ -175,10 +181,17 @@ export function buildReferenceSpec(
|
|
|
175
181
|
else delete doc.components;
|
|
176
182
|
// One requirement with both schemes means both are needed together.
|
|
177
183
|
doc.security = Object.keys(requirement).length > 0 ? [requirement] : [];
|
|
178
|
-
for (const [, item, method] of operationsOf(paths)) {
|
|
184
|
+
for (const [path, item, method] of operationsOf(paths)) {
|
|
179
185
|
const operation = item[method] as Json;
|
|
180
186
|
if (Array.isArray(operation.security) && operation.security.length > 0)
|
|
181
187
|
delete operation.security;
|
|
188
|
+
// The page's code samples add this header, so a copied sample works once the key is filled in (API-120).
|
|
189
|
+
if (
|
|
190
|
+
options.apiKeyRequired &&
|
|
191
|
+
!options.publicOperations.has(actionKey(method, path))
|
|
192
|
+
)
|
|
193
|
+
operation[KEY_HEADER_EXTENSION] = options.apiKeyHeader;
|
|
194
|
+
else delete operation[KEY_HEADER_EXTENSION];
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
return JSON.parse(hideOrigin(JSON.stringify(doc), options)) as Json;
|
package/src/config/v2/init.ts
CHANGED
|
@@ -59,7 +59,7 @@ title: Welcome
|
|
|
59
59
|
|
|
60
60
|
# ${name}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Welcome to the ${name} developer portal. Here you will find guides, the API reference, and everything you need to get an API key and make your first call.
|
|
63
63
|
`;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -9,15 +9,22 @@ export function starterLandingPage(options: {
|
|
|
9
9
|
}): string {
|
|
10
10
|
const name = JSON.stringify(options.name);
|
|
11
11
|
const plansImport = options.withPlans
|
|
12
|
-
? 'import {
|
|
12
|
+
? 'import {\n useGatewayCall,\n usePlansPreview,\n} from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
|
|
13
13
|
: "";
|
|
14
14
|
const plansHook = options.withPlans
|
|
15
|
-
? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n\n"
|
|
15
|
+
? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n // The real gateway address and key header, so the sample works when it is pasted with a key.\n const gateway = useGatewayCall();\n\n"
|
|
16
16
|
: "";
|
|
17
|
+
// Without the plans service the page cannot learn the gateway, so the sample names the platform's default header.
|
|
18
|
+
const sampleUrl = options.withPlans
|
|
19
|
+
? '${gateway.baseUrl ?? "https://YOUR-GATEWAY"}'
|
|
20
|
+
: "https://YOUR-GATEWAY";
|
|
21
|
+
const sampleHeader = options.withPlans ? "${gateway.keyHeader}" : "X-API-Key";
|
|
17
22
|
const plansSection = options.withPlans
|
|
18
23
|
? " <PlansPreviewSection plans={plans} />\n"
|
|
19
24
|
: "";
|
|
20
|
-
return
|
|
25
|
+
return `// Your portal's home page. Edit this file (apitogo/portal/landing/LandingPage.tsx) to make it yours: everything
|
|
26
|
+
// on it is public, so keep notes for yourself in comments like this one.
|
|
27
|
+
import {
|
|
21
28
|
ApiShowcaseSection,
|
|
22
29
|
FeaturesSection,
|
|
23
30
|
HeroSection,
|
|
@@ -47,8 +54,8 @@ ${plansHook} return (
|
|
|
47
54
|
{
|
|
48
55
|
filename: "Call the API",
|
|
49
56
|
language: "bash",
|
|
50
|
-
code: \`$ curl
|
|
51
|
-
-H "
|
|
57
|
+
code: \`$ curl ${sampleUrl}/health \\\\
|
|
58
|
+
-H "${sampleHeader}: YOUR_API_KEY"
|
|
52
59
|
# { "status": "ok" }\`,
|
|
53
60
|
},
|
|
54
61
|
]}
|
|
@@ -58,7 +65,7 @@ ${plansHook} return (
|
|
|
58
65
|
label: "What you get",
|
|
59
66
|
title: "A developer portal that is ready on day one",
|
|
60
67
|
description:
|
|
61
|
-
"
|
|
68
|
+
"Docs, a live API reference, self-service API keys and clear plans, all in one place.",
|
|
62
69
|
}}
|
|
63
70
|
features={[
|
|
64
71
|
{
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -41,6 +41,18 @@ export function contentGlob(configDir: string, glob: string): string {
|
|
|
41
41
|
return `${negated ? "!" : ""}/${CONTENT_LINK}/${relative}`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A project file as the generated site root sees it: `content/<path in the project>`, which the build resolves against
|
|
46
|
+
* the site root. The generated config is imported by the portal's code, so an absolute path would end up in the public
|
|
47
|
+
* bundle (API-127). A file outside the project has no such path and keeps its absolute one.
|
|
48
|
+
*/
|
|
49
|
+
export function contentFile(configDir: string, file: string): string {
|
|
50
|
+
const relative = path.relative(configDir, file);
|
|
51
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
52
|
+
return file.split(path.sep).join("/");
|
|
53
|
+
return `${CONTENT_LINK}/${relative.split(path.sep).join("/")}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/** Capabilities of the plans service that put something on the console. Any one of them registers the plans plugin. */
|
|
45
57
|
const PLANS_CONSOLE_CAPABILITIES = [
|
|
46
58
|
"catalog",
|
|
@@ -77,6 +89,10 @@ export type PortalPlan = {
|
|
|
77
89
|
billing?: string;
|
|
78
90
|
/** Where the account pages link to: only places this portal has (hosted docs, an external docs site, the API page). */
|
|
79
91
|
links?: { docs?: string; api?: string };
|
|
92
|
+
/** The header API users send their key in, from the gateway entry of services.json (API-120). */
|
|
93
|
+
keyHeader?: string;
|
|
94
|
+
/** The gateway's address, for code samples on the portal's pages. */
|
|
95
|
+
gatewayBaseUrl?: string;
|
|
80
96
|
};
|
|
81
97
|
/** Account pages. */
|
|
82
98
|
userPanel: boolean;
|
|
@@ -259,6 +275,10 @@ export function planPortal(
|
|
|
259
275
|
apiBaseUrl: plans.apiBaseUrl,
|
|
260
276
|
keysApiBaseUrl: plans.keysApiBaseUrl,
|
|
261
277
|
billing: plans.billing,
|
|
278
|
+
...(gateway?.keyHeaderName ? { keyHeader: gateway.keyHeaderName } : {}),
|
|
279
|
+
...(gateway?.baseUrl && serviceReady(gateway)
|
|
280
|
+
? { gatewayBaseUrl: gateway.baseUrl }
|
|
281
|
+
: {}),
|
|
262
282
|
links: {
|
|
263
283
|
// Hosted docs are linked once their pages are known (see `buildPortalWorkspace`): there is no page to
|
|
264
284
|
// name before that, and a guessed one was a 404 on every portal whose first page is not called `introduction`.
|
|
@@ -326,14 +346,14 @@ export function buildSiteConfig(
|
|
|
326
346
|
},
|
|
327
347
|
modules: {
|
|
328
348
|
docs: { enabled: plan.docs },
|
|
329
|
-
// A custom home page (`portal.landing`) is served at `/`; the build imports the component
|
|
349
|
+
// A custom home page (`portal.landing`) is served at `/`; the build imports the component through the content link.
|
|
330
350
|
...(portal?.landing
|
|
331
351
|
? {
|
|
332
352
|
landing: {
|
|
333
353
|
enabled: true,
|
|
334
354
|
path: "/",
|
|
335
355
|
layout: "landing",
|
|
336
|
-
page: portal.landing
|
|
356
|
+
page: contentFile(resolved.configDir, portal.landing),
|
|
337
357
|
},
|
|
338
358
|
}
|
|
339
359
|
: {}),
|
|
@@ -1,135 +1,147 @@
|
|
|
1
|
-
import { HTTPSnippet } from "@zudoku/httpsnippet";
|
|
2
|
-
import type { OperationsFragmentFragment } from "../graphql/graphql.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case "
|
|
109
|
-
converted = snippet.convert("
|
|
110
|
-
break;
|
|
111
|
-
case "
|
|
112
|
-
converted = snippet.convert("
|
|
113
|
-
break;
|
|
114
|
-
case "
|
|
115
|
-
converted = snippet.convert("
|
|
116
|
-
break;
|
|
117
|
-
case "
|
|
118
|
-
converted = snippet.convert("
|
|
119
|
-
break;
|
|
120
|
-
case "
|
|
121
|
-
converted = snippet.convert("
|
|
122
|
-
break;
|
|
123
|
-
case "
|
|
124
|
-
converted = snippet.convert("
|
|
125
|
-
break;
|
|
126
|
-
case "
|
|
127
|
-
converted = snippet.convert("
|
|
128
|
-
break;
|
|
129
|
-
|
|
130
|
-
converted = snippet.convert("
|
|
131
|
-
break;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1
|
+
import { HTTPSnippet } from "@zudoku/httpsnippet";
|
|
2
|
+
import type { OperationsFragmentFragment } from "../graphql/graphql.js";
|
|
3
|
+
|
|
4
|
+
/** Operation extension with the header a key goes in (same name as `KEY_HEADER_EXTENSION` in the CLI's config). */
|
|
5
|
+
export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
|
|
6
|
+
|
|
7
|
+
/** What a code sample shows where the visitor's key goes. */
|
|
8
|
+
export const KEY_PLACEHOLDER = "YOUR_API_KEY";
|
|
9
|
+
|
|
10
|
+
const toFormDataParams = (text?: string) => {
|
|
11
|
+
const stringify = (v: unknown) =>
|
|
12
|
+
typeof v === "string" ? v : JSON.stringify(v);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const obj = text && JSON.parse(text);
|
|
16
|
+
if (typeof obj !== "object" || !obj) return [];
|
|
17
|
+
|
|
18
|
+
return Object.entries(obj).flatMap(([name, value]) => {
|
|
19
|
+
const values = Array.isArray(value) ? value : [value];
|
|
20
|
+
return values.map((v) => ({ name, value: stringify(v) }));
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createHttpSnippet = ({
|
|
28
|
+
operation,
|
|
29
|
+
selectedServer,
|
|
30
|
+
exampleBody,
|
|
31
|
+
}: {
|
|
32
|
+
operation: OperationsFragmentFragment;
|
|
33
|
+
selectedServer: string;
|
|
34
|
+
exampleBody: {
|
|
35
|
+
mimeType: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
};
|
|
38
|
+
}) => {
|
|
39
|
+
const isMultipart =
|
|
40
|
+
exampleBody.mimeType === "multipart/form-data" ||
|
|
41
|
+
exampleBody.mimeType === "application/x-www-form-urlencoded";
|
|
42
|
+
|
|
43
|
+
const postData = isMultipart
|
|
44
|
+
? {
|
|
45
|
+
mimeType: exampleBody.mimeType,
|
|
46
|
+
params: toFormDataParams(exampleBody.text),
|
|
47
|
+
}
|
|
48
|
+
: exampleBody;
|
|
49
|
+
|
|
50
|
+
const keyHeader = operation.extensions?.[KEY_HEADER_EXTENSION];
|
|
51
|
+
|
|
52
|
+
return new HTTPSnippet({
|
|
53
|
+
method: operation.method.toUpperCase(),
|
|
54
|
+
url:
|
|
55
|
+
selectedServer + operation.path.replaceAll("{", ":").replaceAll("}", ""),
|
|
56
|
+
postData,
|
|
57
|
+
headers: [
|
|
58
|
+
// The gateway's key header, on operations that need a key (set by the portal build, API-120).
|
|
59
|
+
...(typeof keyHeader === "string" && keyHeader
|
|
60
|
+
? [{ name: keyHeader, value: KEY_PLACEHOLDER }]
|
|
61
|
+
: []),
|
|
62
|
+
...(exampleBody.text
|
|
63
|
+
? [{ name: "Content-Type", value: exampleBody.mimeType }]
|
|
64
|
+
: []),
|
|
65
|
+
...(operation.parameters
|
|
66
|
+
?.filter((p) => p.in === "header" && p.required === true)
|
|
67
|
+
.map((p) => ({
|
|
68
|
+
name: p.name,
|
|
69
|
+
value:
|
|
70
|
+
p.schema?.default ??
|
|
71
|
+
p.examples?.find((x) => x.value)?.value ??
|
|
72
|
+
(p.schema?.type === "string"
|
|
73
|
+
? "<string>"
|
|
74
|
+
: p.schema?.type === "number" || p.schema?.type === "integer"
|
|
75
|
+
? "<number>"
|
|
76
|
+
: p.schema?.type === "boolean"
|
|
77
|
+
? "<bool>"
|
|
78
|
+
: "<value>"),
|
|
79
|
+
})) ?? []),
|
|
80
|
+
],
|
|
81
|
+
queryString:
|
|
82
|
+
operation.parameters
|
|
83
|
+
?.filter((p) => p.in === "query" && p.required === true)
|
|
84
|
+
.map((p) => ({
|
|
85
|
+
name: p.name,
|
|
86
|
+
value:
|
|
87
|
+
p.schema?.default ??
|
|
88
|
+
p.examples?.find((x) => x.value)?.value ??
|
|
89
|
+
(p.schema?.type === "string"
|
|
90
|
+
? "<string>"
|
|
91
|
+
: p.schema?.type === "number" || p.schema?.type === "integer"
|
|
92
|
+
? "<number>"
|
|
93
|
+
: p.schema?.type === "boolean"
|
|
94
|
+
? "<bool>"
|
|
95
|
+
: "<value>"),
|
|
96
|
+
})) ?? [],
|
|
97
|
+
httpVersion: "",
|
|
98
|
+
cookies: [],
|
|
99
|
+
headersSize: 0,
|
|
100
|
+
bodySize: 0,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const getConverted = (snippet: HTTPSnippet, option: string) => {
|
|
105
|
+
// biome-ignore lint/suspicious/noExplicitAny: Allow any type
|
|
106
|
+
let converted: any;
|
|
107
|
+
switch (option) {
|
|
108
|
+
case "shell":
|
|
109
|
+
converted = snippet.convert("shell", "curl");
|
|
110
|
+
break;
|
|
111
|
+
case "js":
|
|
112
|
+
converted = snippet.convert("javascript", "fetch");
|
|
113
|
+
break;
|
|
114
|
+
case "python":
|
|
115
|
+
converted = snippet.convert("python", "requests");
|
|
116
|
+
break;
|
|
117
|
+
case "java":
|
|
118
|
+
converted = snippet.convert("java", "okhttp");
|
|
119
|
+
break;
|
|
120
|
+
case "go":
|
|
121
|
+
converted = snippet.convert("go", "native");
|
|
122
|
+
break;
|
|
123
|
+
case "csharp":
|
|
124
|
+
converted = snippet.convert("csharp", "httpclient");
|
|
125
|
+
break;
|
|
126
|
+
case "kotlin":
|
|
127
|
+
converted = snippet.convert("kotlin", "okhttp");
|
|
128
|
+
break;
|
|
129
|
+
case "objc":
|
|
130
|
+
converted = snippet.convert("objc", "nsurlsession");
|
|
131
|
+
break;
|
|
132
|
+
case "php":
|
|
133
|
+
converted = snippet.convert("php", "http2");
|
|
134
|
+
break;
|
|
135
|
+
case "ruby":
|
|
136
|
+
converted = snippet.convert("ruby");
|
|
137
|
+
break;
|
|
138
|
+
case "swift":
|
|
139
|
+
converted = snippet.convert("swift");
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
converted = snippet.convert("shell");
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return converted ? converted[0] : "";
|
|
147
|
+
};
|