api-spec-cli 0.2.3 → 0.2.4
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/README.md +343 -282
- package/package.json +53 -41
- package/src/cli.js +183 -172
- package/src/commands/add.js +161 -78
- package/src/commands/auth.js +32 -0
- package/src/commands/call.js +220 -207
- package/src/commands/fetch.js +344 -308
- package/src/commands/grep.js +67 -64
- package/src/commands/list.js +78 -80
- package/src/commands/show.js +224 -215
- package/src/commands/specs.js +82 -69
- package/src/commands/types.js +167 -163
- package/src/commands/validate.js +295 -269
- package/src/glob.js +34 -29
- package/src/mcp-client.js +88 -63
- package/src/oauth/auth-flow.js +59 -0
- package/src/oauth/provider.js +192 -0
- package/src/oauth/tokens.js +53 -0
- package/src/registry.js +79 -53
- package/src/resolve.js +64 -65
package/src/commands/fetch.js
CHANGED
|
@@ -1,308 +1,344 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "fs";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { parseKV } from "../args.js";
|
|
5
|
-
import { createMcpClient } from "../mcp-client.js";
|
|
6
|
-
import { matchFilter } from "../glob.js";
|
|
7
|
-
|
|
8
|
-
const INTROSPECTION_QUERY = `{
|
|
9
|
-
__schema {
|
|
10
|
-
queryType { name }
|
|
11
|
-
mutationType { name }
|
|
12
|
-
subscriptionType { name }
|
|
13
|
-
types {
|
|
14
|
-
name
|
|
15
|
-
kind
|
|
16
|
-
description
|
|
17
|
-
fields(includeDeprecated: true) {
|
|
18
|
-
name
|
|
19
|
-
description
|
|
20
|
-
isDeprecated
|
|
21
|
-
deprecationReason
|
|
22
|
-
args {
|
|
23
|
-
name
|
|
24
|
-
description
|
|
25
|
-
type { ...TypeRef }
|
|
26
|
-
defaultValue
|
|
27
|
-
}
|
|
28
|
-
type { ...TypeRef }
|
|
29
|
-
}
|
|
30
|
-
inputFields {
|
|
31
|
-
name
|
|
32
|
-
description
|
|
33
|
-
type { ...TypeRef }
|
|
34
|
-
defaultValue
|
|
35
|
-
}
|
|
36
|
-
enumValues(includeDeprecated: true) {
|
|
37
|
-
name
|
|
38
|
-
description
|
|
39
|
-
isDeprecated
|
|
40
|
-
deprecationReason
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
fragment TypeRef on __Type {
|
|
47
|
-
kind
|
|
48
|
-
name
|
|
49
|
-
ofType {
|
|
50
|
-
kind
|
|
51
|
-
name
|
|
52
|
-
ofType {
|
|
53
|
-
kind
|
|
54
|
-
name
|
|
55
|
-
ofType {
|
|
56
|
-
kind
|
|
57
|
-
name
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}`;
|
|
62
|
-
|
|
63
|
-
function applyFilter(items, nameFn, allowed, disabled) {
|
|
64
|
-
let result = items;
|
|
65
|
-
if (allowed?.length)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* { type: "
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
type: "
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { parseKV } from "../args.js";
|
|
5
|
+
import { createMcpClient } from "../mcp-client.js";
|
|
6
|
+
import { matchFilter } from "../glob.js";
|
|
7
|
+
|
|
8
|
+
const INTROSPECTION_QUERY = `{
|
|
9
|
+
__schema {
|
|
10
|
+
queryType { name }
|
|
11
|
+
mutationType { name }
|
|
12
|
+
subscriptionType { name }
|
|
13
|
+
types {
|
|
14
|
+
name
|
|
15
|
+
kind
|
|
16
|
+
description
|
|
17
|
+
fields(includeDeprecated: true) {
|
|
18
|
+
name
|
|
19
|
+
description
|
|
20
|
+
isDeprecated
|
|
21
|
+
deprecationReason
|
|
22
|
+
args {
|
|
23
|
+
name
|
|
24
|
+
description
|
|
25
|
+
type { ...TypeRef }
|
|
26
|
+
defaultValue
|
|
27
|
+
}
|
|
28
|
+
type { ...TypeRef }
|
|
29
|
+
}
|
|
30
|
+
inputFields {
|
|
31
|
+
name
|
|
32
|
+
description
|
|
33
|
+
type { ...TypeRef }
|
|
34
|
+
defaultValue
|
|
35
|
+
}
|
|
36
|
+
enumValues(includeDeprecated: true) {
|
|
37
|
+
name
|
|
38
|
+
description
|
|
39
|
+
isDeprecated
|
|
40
|
+
deprecationReason
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fragment TypeRef on __Type {
|
|
47
|
+
kind
|
|
48
|
+
name
|
|
49
|
+
ofType {
|
|
50
|
+
kind
|
|
51
|
+
name
|
|
52
|
+
ofType {
|
|
53
|
+
kind
|
|
54
|
+
name
|
|
55
|
+
ofType {
|
|
56
|
+
kind
|
|
57
|
+
name
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}`;
|
|
62
|
+
|
|
63
|
+
function applyFilter(items, nameFn, allowed, disabled) {
|
|
64
|
+
let result = items;
|
|
65
|
+
if (allowed?.length)
|
|
66
|
+
result = result.filter((item) => allowed.some((p) => matchFilter(p, nameFn(item))));
|
|
67
|
+
if (disabled?.length)
|
|
68
|
+
result = result.filter((item) => !disabled.some((p) => matchFilter(p, nameFn(item))));
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a spec from a registry entry or inline flags entry.
|
|
74
|
+
* Entry shape:
|
|
75
|
+
* { type: "openapi", source: "<url-or-file>", config: { headers, auth } }
|
|
76
|
+
* { type: "graphql", source: "<url>", config: { headers, auth } }
|
|
77
|
+
* { _section: "mcp", type: "stdio"|"sse"|"http", url?, command?, args?, env?, headers? }
|
|
78
|
+
*/
|
|
79
|
+
export async function fetchSpec(entry) {
|
|
80
|
+
if (entry._section === "mcp") return await loadMCPFromEntry(entry);
|
|
81
|
+
if (entry._section === "graphql") {
|
|
82
|
+
const spec = await loadGraphQL(entry.source, entry.config?.headers);
|
|
83
|
+
return {
|
|
84
|
+
...spec,
|
|
85
|
+
operations: applyFilter(
|
|
86
|
+
spec.operations,
|
|
87
|
+
(op) => op.name,
|
|
88
|
+
entry.config?.allowedTools,
|
|
89
|
+
entry.config?.disabledTools
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// openapi
|
|
94
|
+
const isUrl = entry.source?.startsWith("http://") || entry.source?.startsWith("https://");
|
|
95
|
+
const spec = isUrl ? await loadFromUrl(entry.source, true) : loadFromFile(entry.source);
|
|
96
|
+
return {
|
|
97
|
+
...spec,
|
|
98
|
+
operations: applyFilter(
|
|
99
|
+
spec.operations,
|
|
100
|
+
(op) => op.id,
|
|
101
|
+
entry.config?.allowedTools,
|
|
102
|
+
entry.config?.disabledTools
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build an inline entry from flags (for ad-hoc commands like --mcp-http <url>).
|
|
109
|
+
* Returns null if no inline source flags present.
|
|
110
|
+
*/
|
|
111
|
+
export function inlineEntryFromFlags(flags) {
|
|
112
|
+
const allowed = flags["allow-tool"];
|
|
113
|
+
const disabled = flags["disable-tool"];
|
|
114
|
+
const filterConfig = {
|
|
115
|
+
...(allowed?.length ? { allowedTools: allowed } : {}),
|
|
116
|
+
...(disabled?.length ? { disabledTools: disabled } : {}),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (flags["mcp-stdio"]) {
|
|
120
|
+
const raw = flags["mcp-stdio"];
|
|
121
|
+
const parts = (raw.trim() ? raw.match(/(?:[^\s"]+|"[^"]*")+/g) : null)?.map((p) =>
|
|
122
|
+
p.replace(/^"|"$/g, "")
|
|
123
|
+
);
|
|
124
|
+
if (!parts?.length) throw new Error("--mcp-stdio requires a non-empty command string");
|
|
125
|
+
return {
|
|
126
|
+
_section: "mcp",
|
|
127
|
+
type: "stdio",
|
|
128
|
+
command: parts[0],
|
|
129
|
+
args: parts.slice(1),
|
|
130
|
+
cwd: flags.cwd,
|
|
131
|
+
env: parseKV(flags.env),
|
|
132
|
+
...filterConfig,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (flags["mcp-sse"]) {
|
|
136
|
+
const headers = parseKV(flags.header);
|
|
137
|
+
if (flags.auth && !headers["Authorization"]) headers["Authorization"] = `Bearer ${flags.auth}`;
|
|
138
|
+
return {
|
|
139
|
+
_section: "mcp",
|
|
140
|
+
type: "sse",
|
|
141
|
+
url: flags["mcp-sse"],
|
|
142
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
143
|
+
...filterConfig,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (flags["mcp-http"]) {
|
|
147
|
+
const headers = parseKV(flags.header);
|
|
148
|
+
if (flags.auth && !headers["Authorization"]) headers["Authorization"] = `Bearer ${flags.auth}`;
|
|
149
|
+
return {
|
|
150
|
+
_section: "mcp",
|
|
151
|
+
type: "http",
|
|
152
|
+
url: flags["mcp-http"],
|
|
153
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
154
|
+
...filterConfig,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (flags.graphql) {
|
|
158
|
+
return {
|
|
159
|
+
_section: "graphql",
|
|
160
|
+
type: "graphql",
|
|
161
|
+
source: flags.graphql,
|
|
162
|
+
config: { headers: parseKV(flags.header), ...filterConfig },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (flags.openapi) {
|
|
166
|
+
return {
|
|
167
|
+
_section: "openapi",
|
|
168
|
+
type: "openapi",
|
|
169
|
+
source: flags.openapi,
|
|
170
|
+
config: {
|
|
171
|
+
headers: parseKV(flags.header),
|
|
172
|
+
baseUrl: flags["base-url"] || null,
|
|
173
|
+
...filterConfig,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Internal loaders ---
|
|
181
|
+
|
|
182
|
+
async function loadMCPFromEntry(entry) {
|
|
183
|
+
const client = await createMcpClient(entry);
|
|
184
|
+
try {
|
|
185
|
+
const { tools } = await client.listTools();
|
|
186
|
+
let mapped = tools.map((t) => ({
|
|
187
|
+
name: t.name,
|
|
188
|
+
description: t.description || null,
|
|
189
|
+
inputSchema: t.inputSchema || null,
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
mapped = applyFilter(mapped, (t) => t.name, entry.allowedTools, entry.disabledTools);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
type: "mcp",
|
|
196
|
+
title: entry.name || "MCP Server",
|
|
197
|
+
transport: entry.type,
|
|
198
|
+
url: entry.url,
|
|
199
|
+
command: entry.command,
|
|
200
|
+
args: entry.args,
|
|
201
|
+
cwd: entry.cwd,
|
|
202
|
+
tools: mapped,
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
await client.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function loadFromUrl(url, skipGraphQLProbe = false) {
|
|
210
|
+
const lowerUrl = url.toLowerCase();
|
|
211
|
+
const isLikelyFile =
|
|
212
|
+
lowerUrl.endsWith(".json") || lowerUrl.endsWith(".yaml") || lowerUrl.endsWith(".yml");
|
|
213
|
+
|
|
214
|
+
if (!isLikelyFile && !skipGraphQLProbe) {
|
|
215
|
+
try {
|
|
216
|
+
return await loadGraphQL(url);
|
|
217
|
+
} catch {
|
|
218
|
+
// Fall through to OpenAPI
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const res = await fetch(url);
|
|
223
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
224
|
+
const text = await res.text();
|
|
225
|
+
return parseOpenAPI(text, url);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function loadFromFile(path) {
|
|
229
|
+
const abs = resolve(path);
|
|
230
|
+
if (!existsSync(abs)) throw new Error(`File not found: ${abs}`);
|
|
231
|
+
const text = readFileSync(abs, "utf-8");
|
|
232
|
+
return parseOpenAPI(text, path);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function loadGraphQL(url, extraHeaders = {}) {
|
|
236
|
+
const res = await fetch(url, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
239
|
+
body: JSON.stringify({ query: INTROSPECTION_QUERY }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!res.ok) throw new Error(`GraphQL introspection failed: HTTP ${res.status}`);
|
|
243
|
+
|
|
244
|
+
const json = await res.json();
|
|
245
|
+
if (json.errors && !json.data) {
|
|
246
|
+
throw new Error(`GraphQL error: ${json.errors[0].message}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const schema = json.data.__schema;
|
|
250
|
+
const operations = [];
|
|
251
|
+
const typeMap = {};
|
|
252
|
+
for (const t of schema.types) {
|
|
253
|
+
typeMap[t.name] = t;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const [kind, rootType] of [
|
|
257
|
+
["query", schema.queryType],
|
|
258
|
+
["mutation", schema.mutationType],
|
|
259
|
+
["subscription", schema.subscriptionType],
|
|
260
|
+
]) {
|
|
261
|
+
if (!rootType) continue;
|
|
262
|
+
const type = typeMap[rootType.name];
|
|
263
|
+
if (!type || !type.fields) continue;
|
|
264
|
+
for (const field of type.fields) {
|
|
265
|
+
operations.push({
|
|
266
|
+
kind,
|
|
267
|
+
name: field.name,
|
|
268
|
+
description: field.description,
|
|
269
|
+
args: field.args,
|
|
270
|
+
returnType: flattenType(field.type),
|
|
271
|
+
isDeprecated: field.isDeprecated,
|
|
272
|
+
deprecationReason: field.deprecationReason,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
type: "graphql",
|
|
279
|
+
title: null,
|
|
280
|
+
endpoint: url,
|
|
281
|
+
operations,
|
|
282
|
+
types: schema.types.filter((t) => !t.name.startsWith("__")),
|
|
283
|
+
raw: schema,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseOpenAPI(text, source) {
|
|
288
|
+
let doc;
|
|
289
|
+
try {
|
|
290
|
+
doc = JSON.parse(text);
|
|
291
|
+
} catch {
|
|
292
|
+
doc = YAML.parse(text);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const version = doc.openapi || doc.swagger;
|
|
296
|
+
if (!version) throw new Error("Not a valid OpenAPI/Swagger spec");
|
|
297
|
+
|
|
298
|
+
const operations = [];
|
|
299
|
+
|
|
300
|
+
for (const [path, methods] of Object.entries(doc.paths || {})) {
|
|
301
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
302
|
+
if (method.startsWith("x-") || method === "parameters") continue;
|
|
303
|
+
operations.push({
|
|
304
|
+
id: op.operationId || `${method.toUpperCase()} ${path}`,
|
|
305
|
+
method: method.toUpperCase(),
|
|
306
|
+
path,
|
|
307
|
+
summary: op.summary || null,
|
|
308
|
+
description: op.description || null,
|
|
309
|
+
parameters: op.parameters || [],
|
|
310
|
+
requestBody: op.requestBody || null,
|
|
311
|
+
responses: op.responses || {},
|
|
312
|
+
tags: op.tags || [],
|
|
313
|
+
deprecated: op.deprecated || false,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
type: "openapi",
|
|
320
|
+
version,
|
|
321
|
+
title: doc.info?.title || null,
|
|
322
|
+
description: doc.info?.description || null,
|
|
323
|
+
servers:
|
|
324
|
+
doc.servers ||
|
|
325
|
+
(doc.host
|
|
326
|
+
? [{ url: `${doc.schemes?.[0] || "https"}://${doc.host}${doc.basePath || ""}` }]
|
|
327
|
+
: []),
|
|
328
|
+
operations,
|
|
329
|
+
components: doc.components || doc.definitions || {},
|
|
330
|
+
raw: doc,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function flattenType(t) {
|
|
335
|
+
if (!t) return null;
|
|
336
|
+
if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
|
|
337
|
+
if (t.ofType) {
|
|
338
|
+
const inner = flattenType(t.ofType);
|
|
339
|
+
if (t.kind === "LIST") return `[${inner}]`;
|
|
340
|
+
if (t.kind === "NON_NULL") return `${inner}!`;
|
|
341
|
+
return inner;
|
|
342
|
+
}
|
|
343
|
+
return t.kind;
|
|
344
|
+
}
|