api-sdk-gen 1.0.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/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +368 -0
- package/dist/index.mjs +374 -0
- package/package.json +35 -0
- package/templates/core.ts +214 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/generators/typescript/util.ts
|
|
5
|
+
function pascalCase(str) {
|
|
6
|
+
return str.replace(/(^\w|-\w)/g, clearAndUpper).replace(/_/g, "");
|
|
7
|
+
}
|
|
8
|
+
function clearAndUpper(text) {
|
|
9
|
+
return text.replace(/-/, "").toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
function resolveSchema(ref) {
|
|
12
|
+
const parts = ref.split("#/components/schemas/");
|
|
13
|
+
return pascalCase(parts[1]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/generators/typescript/schema_to_ts.ts
|
|
17
|
+
function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
18
|
+
return (writer) => {
|
|
19
|
+
if ("$ref" in schema) {
|
|
20
|
+
writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (schema.enum) {
|
|
24
|
+
writer.write(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (schema.anyOf || schema.oneOf) {
|
|
28
|
+
(schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
|
|
29
|
+
if (index > 0) {
|
|
30
|
+
writer.write(" | ");
|
|
31
|
+
}
|
|
32
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(writer);
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (schema.type) {
|
|
37
|
+
switch (schema.type) {
|
|
38
|
+
case "object": {
|
|
39
|
+
const requiredProps = new Set(schema.required || []);
|
|
40
|
+
writer.write("{").newLine();
|
|
41
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
42
|
+
writer.indent(() => {
|
|
43
|
+
writer.write(propName);
|
|
44
|
+
if (!requiredProps.has(propName)) {
|
|
45
|
+
writer.write("?");
|
|
46
|
+
}
|
|
47
|
+
writer.write(": ");
|
|
48
|
+
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
49
|
+
writer.write(";").newLine();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
writer.write("}");
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "array":
|
|
56
|
+
if (Array.isArray(schema.items)) {
|
|
57
|
+
writer.write("[");
|
|
58
|
+
schema.items.forEach((itemSchema, index) => {
|
|
59
|
+
if (index > 0) {
|
|
60
|
+
writer.write(", ");
|
|
61
|
+
}
|
|
62
|
+
schemaToTypeLiteral(itemSchema, doc, refPrefix)(writer);
|
|
63
|
+
});
|
|
64
|
+
writer.write("]");
|
|
65
|
+
} else if (schema.items) {
|
|
66
|
+
writer.write("Array<");
|
|
67
|
+
schemaToTypeLiteral(schema.items, doc, refPrefix)(writer);
|
|
68
|
+
writer.write(">");
|
|
69
|
+
} else {
|
|
70
|
+
writer.write("any[]");
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "string": {
|
|
74
|
+
const s = schema;
|
|
75
|
+
const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
|
|
76
|
+
if (isFile) {
|
|
77
|
+
writer.write("File | Blob");
|
|
78
|
+
} else {
|
|
79
|
+
writer.write(s.type);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "number":
|
|
84
|
+
case "boolean":
|
|
85
|
+
case "null":
|
|
86
|
+
writer.write(schema.type);
|
|
87
|
+
break;
|
|
88
|
+
case "integer":
|
|
89
|
+
writer.write("number");
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
writer.write("any");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/generators/typescript/models_generator.ts
|
|
99
|
+
var jsonSchemaTags = [
|
|
100
|
+
"title",
|
|
101
|
+
"maxItems",
|
|
102
|
+
"minItems",
|
|
103
|
+
"maxLength",
|
|
104
|
+
"minLength",
|
|
105
|
+
"uniqueItems",
|
|
106
|
+
"maximum",
|
|
107
|
+
"minimum",
|
|
108
|
+
"exclusiveMaximum",
|
|
109
|
+
"exclusiveMinimum",
|
|
110
|
+
"pattern",
|
|
111
|
+
"format",
|
|
112
|
+
"example"
|
|
113
|
+
];
|
|
114
|
+
function createInterfaceModel(doc, file, name, schema) {
|
|
115
|
+
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
116
|
+
file.addTypeAlias({
|
|
117
|
+
name: pascalCase(name),
|
|
118
|
+
isExported: true,
|
|
119
|
+
type: "{}"
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const jsdocs = generateSchemaDocs(schema, doc);
|
|
124
|
+
const interfaceDeclaration = file.addInterface({
|
|
125
|
+
name: pascalCase(name),
|
|
126
|
+
isExported: true,
|
|
127
|
+
docs: jsdocs ? [jsdocs] : []
|
|
128
|
+
});
|
|
129
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
130
|
+
const prop = generatePropertySignature(doc, propName, propSchema);
|
|
131
|
+
prop.hasQuestionToken = !(schema.required || []).includes(propName);
|
|
132
|
+
interfaceDeclaration.addProperty(prop);
|
|
133
|
+
}
|
|
134
|
+
if (schema.additionalProperties) {
|
|
135
|
+
interfaceDeclaration.addIndexSignature({
|
|
136
|
+
keyName: "k",
|
|
137
|
+
keyType: "string",
|
|
138
|
+
returnType: "unknown"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function generateSchemaDocs(schema, doc) {
|
|
143
|
+
const jsdoc = {
|
|
144
|
+
description: schema.description || "",
|
|
145
|
+
tags: []
|
|
146
|
+
};
|
|
147
|
+
if ("$ref" in schema) {
|
|
148
|
+
jsdoc.tags?.push({
|
|
149
|
+
tagName: "see",
|
|
150
|
+
text: (writer) => {
|
|
151
|
+
writer.write("{@link ");
|
|
152
|
+
schemaToTypeLiteral(schema, doc)(writer);
|
|
153
|
+
writer.write("}");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
for (const tagName of jsonSchemaTags) {
|
|
158
|
+
const value = schema[tagName];
|
|
159
|
+
if (value !== void 0) {
|
|
160
|
+
jsdoc.tags?.push({
|
|
161
|
+
tagName,
|
|
162
|
+
text: String(value)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return jsdoc;
|
|
170
|
+
}
|
|
171
|
+
function generatePropertySignature(doc, propName, propSchema) {
|
|
172
|
+
const jsdoc = generateSchemaDocs(propSchema, doc);
|
|
173
|
+
return {
|
|
174
|
+
name: propName,
|
|
175
|
+
type: schemaToTypeLiteral(propSchema, doc),
|
|
176
|
+
docs: jsdoc ? [jsdoc] : []
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function createModel(doc, file, name, schema) {
|
|
180
|
+
if ("$ref" in schema) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (schema.type === "object" && schema.properties) {
|
|
184
|
+
createInterfaceModel(doc, file, name, schema);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const jsdoc = generateSchemaDocs(schema, doc);
|
|
188
|
+
file.addTypeAlias({
|
|
189
|
+
name: pascalCase(name),
|
|
190
|
+
isExported: true,
|
|
191
|
+
type: schemaToTypeLiteral(schema, doc),
|
|
192
|
+
docs: jsdoc ? [jsdoc] : []
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function generateModels(file, config, doc) {
|
|
196
|
+
const schemas = doc?.components?.schemas;
|
|
197
|
+
if (!schemas) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
201
|
+
createModel(doc, file, schemaName, schema);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/index.ts
|
|
206
|
+
var import_promises = require("fs/promises");
|
|
207
|
+
var import_ts_morph = require("ts-morph");
|
|
208
|
+
var import_node_path = require("path");
|
|
209
|
+
|
|
210
|
+
// src/generators/typescript/api_generator.ts
|
|
211
|
+
async function generateApiType(file, config, doc) {
|
|
212
|
+
file.addImportDeclaration({
|
|
213
|
+
moduleSpecifier: "./models",
|
|
214
|
+
namespaceImport: "Models",
|
|
215
|
+
isTypeOnly: true
|
|
216
|
+
});
|
|
217
|
+
file.addTypeAlias({
|
|
218
|
+
name: "Api",
|
|
219
|
+
isExported: true,
|
|
220
|
+
type: (writer) => {
|
|
221
|
+
writer.write("{").newLine();
|
|
222
|
+
for (const [path, pathItem] of Object.entries(doc.paths || {})) {
|
|
223
|
+
if (!pathItem) continue;
|
|
224
|
+
writer.indent(() => {
|
|
225
|
+
writer.write(`'${path}': {`).newLine();
|
|
226
|
+
writer.indent(() => {
|
|
227
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
228
|
+
for (const method of methods) {
|
|
229
|
+
const operation = pathItem[method];
|
|
230
|
+
if (!operation) continue;
|
|
231
|
+
writer.write(`${method}: {`).newLine();
|
|
232
|
+
writer.indent(() => {
|
|
233
|
+
const allParams = [...pathItem.parameters || [], ...operation.parameters || []];
|
|
234
|
+
const queryParams = allParams.filter((p) => !("$ref" in p) && p.in === "query");
|
|
235
|
+
const pathParams = allParams.filter((p) => !("$ref" in p) && p.in === "path");
|
|
236
|
+
const headerParams = allParams.filter((p) => !("$ref" in p) && p.in === "header");
|
|
237
|
+
if (queryParams.length || pathParams.length || headerParams.length) {
|
|
238
|
+
writer.write("parameters: {").newLine();
|
|
239
|
+
writer.indent(() => {
|
|
240
|
+
if (pathParams.length) {
|
|
241
|
+
writer.write("path: {").newLine();
|
|
242
|
+
writer.indent(() => {
|
|
243
|
+
for (const p of pathParams) {
|
|
244
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
245
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
246
|
+
else writer.write("any");
|
|
247
|
+
writer.write(";").newLine();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
writer.write("},").newLine();
|
|
251
|
+
}
|
|
252
|
+
if (queryParams.length) {
|
|
253
|
+
writer.write("query: {").newLine();
|
|
254
|
+
writer.indent(() => {
|
|
255
|
+
for (const p of queryParams) {
|
|
256
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
257
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
258
|
+
else writer.write("any");
|
|
259
|
+
writer.write(";").newLine();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
writer.write("},").newLine();
|
|
263
|
+
}
|
|
264
|
+
if (headerParams.length) {
|
|
265
|
+
writer.write("header: {").newLine();
|
|
266
|
+
writer.indent(() => {
|
|
267
|
+
for (const p of headerParams) {
|
|
268
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
269
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
270
|
+
else writer.write("any");
|
|
271
|
+
writer.write(";").newLine();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
writer.write("},").newLine();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
writer.write("},").newLine();
|
|
278
|
+
}
|
|
279
|
+
if (operation.requestBody && !("$ref" in operation.requestBody)) {
|
|
280
|
+
const rb = operation.requestBody;
|
|
281
|
+
writer.write("requestBody: {").newLine();
|
|
282
|
+
writer.indent(() => {
|
|
283
|
+
for (const [contentType, mediaType] of Object.entries(rb.content || {})) {
|
|
284
|
+
writer.write(`'${contentType}': `);
|
|
285
|
+
const mt = mediaType;
|
|
286
|
+
if (mt.schema) schemaToTypeLiteral(mt.schema, doc, "Models.")(writer);
|
|
287
|
+
else writer.write("any");
|
|
288
|
+
writer.write(";").newLine();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
writer.write("},").newLine();
|
|
292
|
+
}
|
|
293
|
+
if (operation.responses) {
|
|
294
|
+
writer.write("responses: {").newLine();
|
|
295
|
+
writer.indent(() => {
|
|
296
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
297
|
+
if ("$ref" in response) continue;
|
|
298
|
+
const res = response;
|
|
299
|
+
writer.write(`'${status}': {`).newLine();
|
|
300
|
+
writer.indent(() => {
|
|
301
|
+
if (res.content) {
|
|
302
|
+
writer.write("content: {").newLine();
|
|
303
|
+
writer.indent(() => {
|
|
304
|
+
for (const [contentType, mediaType] of Object.entries(res.content || {})) {
|
|
305
|
+
writer.write(`'${contentType}': `);
|
|
306
|
+
const mt = mediaType;
|
|
307
|
+
if (mt.schema) schemaToTypeLiteral(mt.schema, doc, "Models.")(writer);
|
|
308
|
+
else writer.write("any");
|
|
309
|
+
writer.write(";").newLine();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
writer.write("}").newLine();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
writer.write("},").newLine();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
writer.write("},").newLine();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
writer.write("},").newLine();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
writer.write("},").newLine();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
writer.write("}");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/index.ts
|
|
333
|
+
var import_commander = require("commander");
|
|
334
|
+
var program = new import_commander.Command();
|
|
335
|
+
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL of the OpenAPI JSON specification").requiredOption("-b, --base <url>", "Base URL for the API calls").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
|
|
336
|
+
const config = {
|
|
337
|
+
url: options.url,
|
|
338
|
+
api_base: options.base,
|
|
339
|
+
outputDir: options.output
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
console.log(`Fetching OpenAPI spec from: ${config.url}...`);
|
|
343
|
+
const res = await fetch(config.url);
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
|
|
346
|
+
}
|
|
347
|
+
const openapi = await res.json();
|
|
348
|
+
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
349
|
+
await (0, import_promises.mkdir)(config.outputDir, { recursive: true });
|
|
350
|
+
const project = new import_ts_morph.Project();
|
|
351
|
+
const modelsFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
352
|
+
const apiFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
353
|
+
const templatePath = (0, import_node_path.join)(__dirname, "../templates", "core.ts");
|
|
354
|
+
const coreFile = project.createSourceFile(
|
|
355
|
+
(0, import_node_path.join)(config.outputDir, "core.ts"),
|
|
356
|
+
await (0, import_promises.readFile)(templatePath, "utf-8"),
|
|
357
|
+
{ overwrite: true }
|
|
358
|
+
);
|
|
359
|
+
await generateModels(modelsFile, config, openapi);
|
|
360
|
+
await generateApiType(apiFile, config, openapi);
|
|
361
|
+
await project.save();
|
|
362
|
+
console.log("SDK generated successfully!");
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("Error generating SDK:", error instanceof Error ? error.message : String(error));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
program.parse();
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// node_modules/.pnpm/tsup@8.5.1_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
7
|
+
var getDirname = () => path.dirname(getFilename());
|
|
8
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
9
|
+
|
|
10
|
+
// src/generators/typescript/util.ts
|
|
11
|
+
function pascalCase(str) {
|
|
12
|
+
return str.replace(/(^\w|-\w)/g, clearAndUpper).replace(/_/g, "");
|
|
13
|
+
}
|
|
14
|
+
function clearAndUpper(text) {
|
|
15
|
+
return text.replace(/-/, "").toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
function resolveSchema(ref) {
|
|
18
|
+
const parts = ref.split("#/components/schemas/");
|
|
19
|
+
return pascalCase(parts[1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/generators/typescript/schema_to_ts.ts
|
|
23
|
+
function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
24
|
+
return (writer) => {
|
|
25
|
+
if ("$ref" in schema) {
|
|
26
|
+
writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (schema.enum) {
|
|
30
|
+
writer.write(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (schema.anyOf || schema.oneOf) {
|
|
34
|
+
(schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
|
|
35
|
+
if (index > 0) {
|
|
36
|
+
writer.write(" | ");
|
|
37
|
+
}
|
|
38
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(writer);
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (schema.type) {
|
|
43
|
+
switch (schema.type) {
|
|
44
|
+
case "object": {
|
|
45
|
+
const requiredProps = new Set(schema.required || []);
|
|
46
|
+
writer.write("{").newLine();
|
|
47
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
48
|
+
writer.indent(() => {
|
|
49
|
+
writer.write(propName);
|
|
50
|
+
if (!requiredProps.has(propName)) {
|
|
51
|
+
writer.write("?");
|
|
52
|
+
}
|
|
53
|
+
writer.write(": ");
|
|
54
|
+
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
55
|
+
writer.write(";").newLine();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
writer.write("}");
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "array":
|
|
62
|
+
if (Array.isArray(schema.items)) {
|
|
63
|
+
writer.write("[");
|
|
64
|
+
schema.items.forEach((itemSchema, index) => {
|
|
65
|
+
if (index > 0) {
|
|
66
|
+
writer.write(", ");
|
|
67
|
+
}
|
|
68
|
+
schemaToTypeLiteral(itemSchema, doc, refPrefix)(writer);
|
|
69
|
+
});
|
|
70
|
+
writer.write("]");
|
|
71
|
+
} else if (schema.items) {
|
|
72
|
+
writer.write("Array<");
|
|
73
|
+
schemaToTypeLiteral(schema.items, doc, refPrefix)(writer);
|
|
74
|
+
writer.write(">");
|
|
75
|
+
} else {
|
|
76
|
+
writer.write("any[]");
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "string": {
|
|
80
|
+
const s = schema;
|
|
81
|
+
const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
|
|
82
|
+
if (isFile) {
|
|
83
|
+
writer.write("File | Blob");
|
|
84
|
+
} else {
|
|
85
|
+
writer.write(s.type);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "number":
|
|
90
|
+
case "boolean":
|
|
91
|
+
case "null":
|
|
92
|
+
writer.write(schema.type);
|
|
93
|
+
break;
|
|
94
|
+
case "integer":
|
|
95
|
+
writer.write("number");
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
writer.write("any");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/generators/typescript/models_generator.ts
|
|
105
|
+
var jsonSchemaTags = [
|
|
106
|
+
"title",
|
|
107
|
+
"maxItems",
|
|
108
|
+
"minItems",
|
|
109
|
+
"maxLength",
|
|
110
|
+
"minLength",
|
|
111
|
+
"uniqueItems",
|
|
112
|
+
"maximum",
|
|
113
|
+
"minimum",
|
|
114
|
+
"exclusiveMaximum",
|
|
115
|
+
"exclusiveMinimum",
|
|
116
|
+
"pattern",
|
|
117
|
+
"format",
|
|
118
|
+
"example"
|
|
119
|
+
];
|
|
120
|
+
function createInterfaceModel(doc, file, name, schema) {
|
|
121
|
+
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
122
|
+
file.addTypeAlias({
|
|
123
|
+
name: pascalCase(name),
|
|
124
|
+
isExported: true,
|
|
125
|
+
type: "{}"
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const jsdocs = generateSchemaDocs(schema, doc);
|
|
130
|
+
const interfaceDeclaration = file.addInterface({
|
|
131
|
+
name: pascalCase(name),
|
|
132
|
+
isExported: true,
|
|
133
|
+
docs: jsdocs ? [jsdocs] : []
|
|
134
|
+
});
|
|
135
|
+
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
136
|
+
const prop = generatePropertySignature(doc, propName, propSchema);
|
|
137
|
+
prop.hasQuestionToken = !(schema.required || []).includes(propName);
|
|
138
|
+
interfaceDeclaration.addProperty(prop);
|
|
139
|
+
}
|
|
140
|
+
if (schema.additionalProperties) {
|
|
141
|
+
interfaceDeclaration.addIndexSignature({
|
|
142
|
+
keyName: "k",
|
|
143
|
+
keyType: "string",
|
|
144
|
+
returnType: "unknown"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function generateSchemaDocs(schema, doc) {
|
|
149
|
+
const jsdoc = {
|
|
150
|
+
description: schema.description || "",
|
|
151
|
+
tags: []
|
|
152
|
+
};
|
|
153
|
+
if ("$ref" in schema) {
|
|
154
|
+
jsdoc.tags?.push({
|
|
155
|
+
tagName: "see",
|
|
156
|
+
text: (writer) => {
|
|
157
|
+
writer.write("{@link ");
|
|
158
|
+
schemaToTypeLiteral(schema, doc)(writer);
|
|
159
|
+
writer.write("}");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
for (const tagName of jsonSchemaTags) {
|
|
164
|
+
const value = schema[tagName];
|
|
165
|
+
if (value !== void 0) {
|
|
166
|
+
jsdoc.tags?.push({
|
|
167
|
+
tagName,
|
|
168
|
+
text: String(value)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return jsdoc;
|
|
176
|
+
}
|
|
177
|
+
function generatePropertySignature(doc, propName, propSchema) {
|
|
178
|
+
const jsdoc = generateSchemaDocs(propSchema, doc);
|
|
179
|
+
return {
|
|
180
|
+
name: propName,
|
|
181
|
+
type: schemaToTypeLiteral(propSchema, doc),
|
|
182
|
+
docs: jsdoc ? [jsdoc] : []
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function createModel(doc, file, name, schema) {
|
|
186
|
+
if ("$ref" in schema) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (schema.type === "object" && schema.properties) {
|
|
190
|
+
createInterfaceModel(doc, file, name, schema);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const jsdoc = generateSchemaDocs(schema, doc);
|
|
194
|
+
file.addTypeAlias({
|
|
195
|
+
name: pascalCase(name),
|
|
196
|
+
isExported: true,
|
|
197
|
+
type: schemaToTypeLiteral(schema, doc),
|
|
198
|
+
docs: jsdoc ? [jsdoc] : []
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function generateModels(file, config, doc) {
|
|
202
|
+
const schemas = doc?.components?.schemas;
|
|
203
|
+
if (!schemas) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
207
|
+
createModel(doc, file, schemaName, schema);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/index.ts
|
|
212
|
+
import { mkdir, readFile } from "fs/promises";
|
|
213
|
+
import { Project } from "ts-morph";
|
|
214
|
+
import { join } from "path";
|
|
215
|
+
|
|
216
|
+
// src/generators/typescript/api_generator.ts
|
|
217
|
+
async function generateApiType(file, config, doc) {
|
|
218
|
+
file.addImportDeclaration({
|
|
219
|
+
moduleSpecifier: "./models",
|
|
220
|
+
namespaceImport: "Models",
|
|
221
|
+
isTypeOnly: true
|
|
222
|
+
});
|
|
223
|
+
file.addTypeAlias({
|
|
224
|
+
name: "Api",
|
|
225
|
+
isExported: true,
|
|
226
|
+
type: (writer) => {
|
|
227
|
+
writer.write("{").newLine();
|
|
228
|
+
for (const [path2, pathItem] of Object.entries(doc.paths || {})) {
|
|
229
|
+
if (!pathItem) continue;
|
|
230
|
+
writer.indent(() => {
|
|
231
|
+
writer.write(`'${path2}': {`).newLine();
|
|
232
|
+
writer.indent(() => {
|
|
233
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
234
|
+
for (const method of methods) {
|
|
235
|
+
const operation = pathItem[method];
|
|
236
|
+
if (!operation) continue;
|
|
237
|
+
writer.write(`${method}: {`).newLine();
|
|
238
|
+
writer.indent(() => {
|
|
239
|
+
const allParams = [...pathItem.parameters || [], ...operation.parameters || []];
|
|
240
|
+
const queryParams = allParams.filter((p) => !("$ref" in p) && p.in === "query");
|
|
241
|
+
const pathParams = allParams.filter((p) => !("$ref" in p) && p.in === "path");
|
|
242
|
+
const headerParams = allParams.filter((p) => !("$ref" in p) && p.in === "header");
|
|
243
|
+
if (queryParams.length || pathParams.length || headerParams.length) {
|
|
244
|
+
writer.write("parameters: {").newLine();
|
|
245
|
+
writer.indent(() => {
|
|
246
|
+
if (pathParams.length) {
|
|
247
|
+
writer.write("path: {").newLine();
|
|
248
|
+
writer.indent(() => {
|
|
249
|
+
for (const p of pathParams) {
|
|
250
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
251
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
252
|
+
else writer.write("any");
|
|
253
|
+
writer.write(";").newLine();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
writer.write("},").newLine();
|
|
257
|
+
}
|
|
258
|
+
if (queryParams.length) {
|
|
259
|
+
writer.write("query: {").newLine();
|
|
260
|
+
writer.indent(() => {
|
|
261
|
+
for (const p of queryParams) {
|
|
262
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
263
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
264
|
+
else writer.write("any");
|
|
265
|
+
writer.write(";").newLine();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
writer.write("},").newLine();
|
|
269
|
+
}
|
|
270
|
+
if (headerParams.length) {
|
|
271
|
+
writer.write("header: {").newLine();
|
|
272
|
+
writer.indent(() => {
|
|
273
|
+
for (const p of headerParams) {
|
|
274
|
+
writer.write(`'${p.name}'${p.required ? "" : "?"}: `);
|
|
275
|
+
if (p.schema) schemaToTypeLiteral(p.schema, doc, "Models.")(writer);
|
|
276
|
+
else writer.write("any");
|
|
277
|
+
writer.write(";").newLine();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
writer.write("},").newLine();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
writer.write("},").newLine();
|
|
284
|
+
}
|
|
285
|
+
if (operation.requestBody && !("$ref" in operation.requestBody)) {
|
|
286
|
+
const rb = operation.requestBody;
|
|
287
|
+
writer.write("requestBody: {").newLine();
|
|
288
|
+
writer.indent(() => {
|
|
289
|
+
for (const [contentType, mediaType] of Object.entries(rb.content || {})) {
|
|
290
|
+
writer.write(`'${contentType}': `);
|
|
291
|
+
const mt = mediaType;
|
|
292
|
+
if (mt.schema) schemaToTypeLiteral(mt.schema, doc, "Models.")(writer);
|
|
293
|
+
else writer.write("any");
|
|
294
|
+
writer.write(";").newLine();
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
writer.write("},").newLine();
|
|
298
|
+
}
|
|
299
|
+
if (operation.responses) {
|
|
300
|
+
writer.write("responses: {").newLine();
|
|
301
|
+
writer.indent(() => {
|
|
302
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
303
|
+
if ("$ref" in response) continue;
|
|
304
|
+
const res = response;
|
|
305
|
+
writer.write(`'${status}': {`).newLine();
|
|
306
|
+
writer.indent(() => {
|
|
307
|
+
if (res.content) {
|
|
308
|
+
writer.write("content: {").newLine();
|
|
309
|
+
writer.indent(() => {
|
|
310
|
+
for (const [contentType, mediaType] of Object.entries(res.content || {})) {
|
|
311
|
+
writer.write(`'${contentType}': `);
|
|
312
|
+
const mt = mediaType;
|
|
313
|
+
if (mt.schema) schemaToTypeLiteral(mt.schema, doc, "Models.")(writer);
|
|
314
|
+
else writer.write("any");
|
|
315
|
+
writer.write(";").newLine();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
writer.write("}").newLine();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
writer.write("},").newLine();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
writer.write("},").newLine();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
writer.write("},").newLine();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
writer.write("},").newLine();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
writer.write("}");
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/index.ts
|
|
339
|
+
import { Command } from "commander";
|
|
340
|
+
var program = new Command();
|
|
341
|
+
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL of the OpenAPI JSON specification").requiredOption("-b, --base <url>", "Base URL for the API calls").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
|
|
342
|
+
const config = {
|
|
343
|
+
url: options.url,
|
|
344
|
+
api_base: options.base,
|
|
345
|
+
outputDir: options.output
|
|
346
|
+
};
|
|
347
|
+
try {
|
|
348
|
+
console.log(`Fetching OpenAPI spec from: ${config.url}...`);
|
|
349
|
+
const res = await fetch(config.url);
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
|
|
352
|
+
}
|
|
353
|
+
const openapi = await res.json();
|
|
354
|
+
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
355
|
+
await mkdir(config.outputDir, { recursive: true });
|
|
356
|
+
const project = new Project();
|
|
357
|
+
const modelsFile = project.createSourceFile(join(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
358
|
+
const apiFile = project.createSourceFile(join(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
359
|
+
const templatePath = join(__dirname, "../templates", "core.ts");
|
|
360
|
+
const coreFile = project.createSourceFile(
|
|
361
|
+
join(config.outputDir, "core.ts"),
|
|
362
|
+
await readFile(templatePath, "utf-8"),
|
|
363
|
+
{ overwrite: true }
|
|
364
|
+
);
|
|
365
|
+
await generateModels(modelsFile, config, openapi);
|
|
366
|
+
await generateApiType(apiFile, config, openapi);
|
|
367
|
+
await project.save();
|
|
368
|
+
console.log("SDK generated successfully!");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error("Error generating SDK:", error instanceof Error ? error.message : String(error));
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-sdk-gen",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"api-sdk-gen": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"templates"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^14.0.3",
|
|
20
|
+
"openapi-types": "^12.1.3",
|
|
21
|
+
"ts-morph": "^27.0.2",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.0.3",
|
|
26
|
+
"tsup": "^8.5.1",
|
|
27
|
+
"tsx": "^4.21.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --clean --format cjs,esm --dts --shims",
|
|
31
|
+
"dev": "tsup src/index.ts --watch",
|
|
32
|
+
"gen": "tsx src/index.ts",
|
|
33
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
2
|
+
|
|
3
|
+
interface BaseOperation {
|
|
4
|
+
parameters?: {
|
|
5
|
+
query?: Record<string, unknown>;
|
|
6
|
+
path?: Record<string, string>;
|
|
7
|
+
header?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
requestBody?: Record<string, any>;
|
|
10
|
+
responses?: Record<string | number, {
|
|
11
|
+
headers?: Record<string, Record<string, any>>;
|
|
12
|
+
content?: Record<string, any>
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ApiDoc = Record<string, Partial<BaseOperation & Record<HttpMethod, BaseOperation>>>;
|
|
17
|
+
|
|
18
|
+
type BaseRequest = {
|
|
19
|
+
path?: Record<string, string>;
|
|
20
|
+
query?: Record<string, string>;
|
|
21
|
+
header?: Record<string, string>;
|
|
22
|
+
contentType?: string;
|
|
23
|
+
body?: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Pretty<T> = {
|
|
27
|
+
[K in keyof T]: T[K];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type DeepPretty<T> = T extends object ? Pretty<{
|
|
31
|
+
[K in keyof T]: DeepPretty<T[K]>;
|
|
32
|
+
}> : T;
|
|
33
|
+
|
|
34
|
+
type GetOp<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod> =
|
|
35
|
+
T[P][M] extends infer Op ? (Op extends BaseOperation ? Op : never) : never;
|
|
36
|
+
|
|
37
|
+
type RequiredKeys<T> = { [K in keyof T]-?: (Record<string, never>) extends Pick<T, K> ? never : K }[keyof T];
|
|
38
|
+
|
|
39
|
+
type OptionalIfEmpty<K extends string, T> =
|
|
40
|
+
[T] extends [never] ? unknown :
|
|
41
|
+
[T] extends [undefined] ? unknown :
|
|
42
|
+
RequiredKeys<T> extends never
|
|
43
|
+
? (keyof T extends never ? unknown : { [P in K]?: T })
|
|
44
|
+
: { [P in K]: T };
|
|
45
|
+
|
|
46
|
+
type OpBody<Op extends BaseOperation> =
|
|
47
|
+
Op['requestBody'] extends infer RB
|
|
48
|
+
? RB extends Record<string, unknown>
|
|
49
|
+
? { [K in keyof RB]: {
|
|
50
|
+
contentType: K,
|
|
51
|
+
body: K extends 'multipart/form-data' ? (RB[K] | FormData) : RB[K]
|
|
52
|
+
} }[keyof RB]
|
|
53
|
+
: unknown
|
|
54
|
+
: unknown;
|
|
55
|
+
|
|
56
|
+
type ToNumber<S> = S extends number ? S : S extends `${infer N extends number}` ? N : S;
|
|
57
|
+
|
|
58
|
+
type ApiFetchResponse<Op extends BaseOperation> =
|
|
59
|
+
Op['responses'] extends infer R
|
|
60
|
+
? R extends Record<string | number, any>
|
|
61
|
+
? {
|
|
62
|
+
[S in keyof R]: S extends number | string
|
|
63
|
+
? R[S] extends { content: infer C }
|
|
64
|
+
? C extends Record<string, any>
|
|
65
|
+
? { [CT in keyof C]: {
|
|
66
|
+
status: ToNumber<S>,
|
|
67
|
+
contentType: CT,
|
|
68
|
+
body: C[CT]
|
|
69
|
+
} }[keyof C]
|
|
70
|
+
: { status: ToNumber<S>, contentType: null, body: null }
|
|
71
|
+
: { status: ToNumber<S>, contentType: null, body: null }
|
|
72
|
+
: never
|
|
73
|
+
}[keyof R]
|
|
74
|
+
: { status: number; contentType: string; body: any }
|
|
75
|
+
: { status: number; contentType: string; body: any };
|
|
76
|
+
|
|
77
|
+
type ApiFetchOptions<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod, Op = GetOp<T, P, M>> =
|
|
78
|
+
{ method: M } &
|
|
79
|
+
(Op extends BaseOperation ?
|
|
80
|
+
OptionalIfEmpty<'path', NonNullable<Op['parameters']>['path']> &
|
|
81
|
+
OptionalIfEmpty<'query', NonNullable<Op['parameters']>['query']> &
|
|
82
|
+
OptionalIfEmpty<'header', NonNullable<Op['parameters']>['header']> &
|
|
83
|
+
OpBody<Op>
|
|
84
|
+
: unknown);
|
|
85
|
+
|
|
86
|
+
type ApiFetch<T extends ApiDoc> = <
|
|
87
|
+
P extends keyof T,
|
|
88
|
+
M extends keyof T[P] & HttpMethod
|
|
89
|
+
>(
|
|
90
|
+
path: P,
|
|
91
|
+
options: ApiFetchOptions<T, P, M>
|
|
92
|
+
) => Promise<DeepPretty<ApiFetchResponse<GetOp<T, P, M>>>>;
|
|
93
|
+
|
|
94
|
+
export type Request = {
|
|
95
|
+
url: URL;
|
|
96
|
+
init: RequestInit;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildApiRequest(
|
|
100
|
+
baseUrl: string | URL,
|
|
101
|
+
path: string,
|
|
102
|
+
options: BaseRequest
|
|
103
|
+
): Request {
|
|
104
|
+
const headers = new Headers();
|
|
105
|
+
const opts = options as any;
|
|
106
|
+
let body: any = opts.body;
|
|
107
|
+
const url = new URL(path as string, baseUrl);
|
|
108
|
+
|
|
109
|
+
if (opts.header) {
|
|
110
|
+
Object.entries(opts.header).forEach(([key, value]) => {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
value.forEach(v => {
|
|
113
|
+
headers.append(key, v)
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
headers.set(key, value as string);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (opts.body) {
|
|
122
|
+
headers.append('Content-Type', opts.contentType);
|
|
123
|
+
|
|
124
|
+
if (opts.contentType === 'application/json') {
|
|
125
|
+
body = JSON.stringify(body);
|
|
126
|
+
} else if (opts.contentType === 'multipart/form-data' && !(body instanceof FormData)) {
|
|
127
|
+
const form = new FormData();
|
|
128
|
+
|
|
129
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
value.forEach(v => {
|
|
132
|
+
form.append(key, v)
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
form.set(key, value as string);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
body = form;
|
|
140
|
+
} else if (opts.contentType === 'application/x-www-form-urlencoded') {
|
|
141
|
+
body = new URLSearchParams(body);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (opts.path) {
|
|
146
|
+
url.pathname = url.pathname.replace(/\{(.+?)\}/g, (text, key) => key in opts.path ? opts.path[key] : text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (opts.query) {
|
|
150
|
+
Object.entries(opts.query).forEach(([key, value]) => {
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
value.forEach(v => {
|
|
153
|
+
url.searchParams.append(key, v)
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
url.searchParams.set(key, value as string);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
url,
|
|
163
|
+
init: {
|
|
164
|
+
method: opts.method.toUpperCase(),
|
|
165
|
+
headers,
|
|
166
|
+
body,
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function buildResponse<T extends ApiFetchResponse<any>>(response: Response): Promise<T> {
|
|
172
|
+
const contentType = response.headers.get('Content-Type') || null;
|
|
173
|
+
|
|
174
|
+
if (contentType?.startsWith('application/json')) {
|
|
175
|
+
return {
|
|
176
|
+
status: response.status,
|
|
177
|
+
contentType,
|
|
178
|
+
body: await response.json(),
|
|
179
|
+
} as T;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (contentType?.startsWith('application/xml')) {
|
|
183
|
+
return {
|
|
184
|
+
status: response.status,
|
|
185
|
+
contentType,
|
|
186
|
+
body: new DOMParser().parseFromString(await response.text(), 'application/xml'),
|
|
187
|
+
} as T;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (contentType?.startsWith('text/plain')) {
|
|
191
|
+
return {
|
|
192
|
+
status: response.status,
|
|
193
|
+
contentType,
|
|
194
|
+
body: await response.text(),
|
|
195
|
+
} as T;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
status: response.status,
|
|
200
|
+
contentType,
|
|
201
|
+
body: await response.blob(),
|
|
202
|
+
} as T;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createApi<T extends ApiDoc>(
|
|
206
|
+
baseUrl: string | URL,
|
|
207
|
+
adapter?: (request: Request) => Promise<Response>
|
|
208
|
+
) {
|
|
209
|
+
adapter ??= (request) => fetch(request.url, request.init);
|
|
210
|
+
|
|
211
|
+
const api = async (path: string, options: any) => buildResponse(await adapter(buildApiRequest(baseUrl, path, options)));
|
|
212
|
+
|
|
213
|
+
return api as ApiFetch<T>;
|
|
214
|
+
}
|