@taico/openapi-sdkgen 0.1.2 → 0.2.12
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/generator.js +197 -24
- package/package.json +1 -1
package/dist/generator.js
CHANGED
|
@@ -53,7 +53,7 @@ function schemaToTypeScript(schema) {
|
|
|
53
53
|
let baseType;
|
|
54
54
|
switch (schema.type) {
|
|
55
55
|
case 'string':
|
|
56
|
-
baseType = 'string';
|
|
56
|
+
baseType = schema.format === 'binary' ? 'Blob' : 'string';
|
|
57
57
|
break;
|
|
58
58
|
case 'number':
|
|
59
59
|
case 'integer':
|
|
@@ -104,9 +104,113 @@ export interface ClientConfig {
|
|
|
104
104
|
headers?: Record<string, string>;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
108
|
+
return typeof value === 'object' && value !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getErrorMessageFromBody(body: unknown): string | undefined {
|
|
112
|
+
if (!isRecord(body)) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const detail = body.detail;
|
|
117
|
+
if (typeof detail === 'string' && detail.length > 0) {
|
|
118
|
+
return detail;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const message = body.message;
|
|
122
|
+
if (typeof message === 'string' && message.length > 0) {
|
|
123
|
+
return message;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const title = body.title;
|
|
127
|
+
if (typeof title === 'string' && title.length > 0) {
|
|
128
|
+
return title;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const errorDescription = body.error_description;
|
|
132
|
+
if (typeof errorDescription === 'string' && errorDescription.length > 0) {
|
|
133
|
+
return errorDescription;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class ApiError extends Error {
|
|
140
|
+
public readonly status: number;
|
|
141
|
+
public readonly statusText: string;
|
|
142
|
+
public readonly body: unknown;
|
|
143
|
+
|
|
144
|
+
constructor(response: Response, body: unknown) {
|
|
145
|
+
const message = getErrorMessageFromBody(body) ?? \`HTTP \${response.status}: \${response.statusText}\`;
|
|
146
|
+
super(message);
|
|
147
|
+
this.name = 'ApiError';
|
|
148
|
+
this.status = response.status;
|
|
149
|
+
this.statusText = response.statusText;
|
|
150
|
+
this.body = body;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
107
154
|
export class BaseClient {
|
|
108
155
|
constructor(protected config: ClientConfig) {}
|
|
109
156
|
|
|
157
|
+
private async parseResponseBody(
|
|
158
|
+
response: Response,
|
|
159
|
+
responseType: 'auto' | 'json' | 'text' | 'arrayBuffer' | 'void' = 'auto'
|
|
160
|
+
): Promise<unknown> {
|
|
161
|
+
if (responseType === 'void') {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (responseType === 'json') {
|
|
170
|
+
try {
|
|
171
|
+
return await response.json();
|
|
172
|
+
} catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (responseType === 'text') {
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
return text.length > 0 ? text : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (responseType === 'arrayBuffer') {
|
|
183
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
184
|
+
return arrayBuffer.byteLength > 0 ? arrayBuffer : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const contentType = response.headers.get('content-type');
|
|
188
|
+
if (contentType?.includes('application/json')) {
|
|
189
|
+
try {
|
|
190
|
+
return await response.json();
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const text = await response.text();
|
|
197
|
+
return text.length > 0 ? text : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private hasHeader(headers: Record<string, string>, headerName: string): boolean {
|
|
201
|
+
const target = headerName.toLowerCase();
|
|
202
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private removeHeader(headers: Record<string, string>, headerName: string): void {
|
|
206
|
+
const target = headerName.toLowerCase();
|
|
207
|
+
for (const key of Object.keys(headers)) {
|
|
208
|
+
if (key.toLowerCase() === target) {
|
|
209
|
+
delete headers[key];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
110
214
|
private async buildAuthHeaders(): Promise<Record<string, string>> {
|
|
111
215
|
const headers: Record<string, string> = {};
|
|
112
216
|
if (this.config.getAccessToken) {
|
|
@@ -125,6 +229,8 @@ export class BaseClient {
|
|
|
125
229
|
options?: {
|
|
126
230
|
params?: Record<string, any>;
|
|
127
231
|
body?: any;
|
|
232
|
+
bodyType?: 'json' | 'form-data' | 'raw';
|
|
233
|
+
responseType?: 'auto' | 'json' | 'text' | 'arrayBuffer' | 'void';
|
|
128
234
|
headers?: Record<string, string>;
|
|
129
235
|
signal?: AbortSignal;
|
|
130
236
|
}
|
|
@@ -164,34 +270,37 @@ export class BaseClient {
|
|
|
164
270
|
// Add auth headers
|
|
165
271
|
Object.assign(headers, await this.buildAuthHeaders());
|
|
166
272
|
|
|
167
|
-
|
|
168
|
-
if (options?.body
|
|
169
|
-
|
|
273
|
+
let requestBody: any;
|
|
274
|
+
if (options?.body !== undefined) {
|
|
275
|
+
const bodyType = options.bodyType ?? 'json';
|
|
276
|
+
|
|
277
|
+
if (bodyType === 'form-data') {
|
|
278
|
+
requestBody = options.body;
|
|
279
|
+
this.removeHeader(headers, 'Content-Type');
|
|
280
|
+
} else if (bodyType === 'raw') {
|
|
281
|
+
requestBody = options.body;
|
|
282
|
+
} else {
|
|
283
|
+
if (!this.hasHeader(headers, 'Content-Type')) {
|
|
284
|
+
headers['Content-Type'] = 'application/json';
|
|
285
|
+
}
|
|
286
|
+
requestBody = JSON.stringify(options.body);
|
|
287
|
+
}
|
|
170
288
|
}
|
|
171
289
|
|
|
172
290
|
const response = await fetch(url.toString(), {
|
|
173
291
|
method,
|
|
174
292
|
headers,
|
|
175
|
-
body:
|
|
293
|
+
body: requestBody,
|
|
176
294
|
signal: options?.signal,
|
|
177
295
|
credentials: this.config.credentials,
|
|
178
296
|
});
|
|
179
297
|
|
|
180
298
|
if (!response.ok) {
|
|
181
|
-
|
|
299
|
+
const errorBody = await this.parseResponseBody(response, 'auto');
|
|
300
|
+
throw new ApiError(response, errorBody);
|
|
182
301
|
}
|
|
183
302
|
|
|
184
|
-
|
|
185
|
-
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
186
|
-
return undefined as T;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const contentType = response.headers.get('content-type');
|
|
190
|
-
if (contentType?.includes('application/json')) {
|
|
191
|
-
return response.json() as Promise<T>;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return response.text() as any;
|
|
303
|
+
return await this.parseResponseBody(response, options?.responseType ?? 'auto') as T;
|
|
195
304
|
}
|
|
196
305
|
|
|
197
306
|
protected async *streamEvents<T = any>(
|
|
@@ -246,7 +355,8 @@ export class BaseClient {
|
|
|
246
355
|
});
|
|
247
356
|
|
|
248
357
|
if (!response.ok) {
|
|
249
|
-
|
|
358
|
+
const errorBody = await this.parseResponseBody(response);
|
|
359
|
+
throw new ApiError(response, errorBody);
|
|
250
360
|
}
|
|
251
361
|
|
|
252
362
|
if (!response.body) {
|
|
@@ -346,11 +456,12 @@ function generateOperation(op) {
|
|
|
346
456
|
const lines = [];
|
|
347
457
|
// Determine if this is a streaming endpoint by checking response content-type
|
|
348
458
|
const isStreaming = isStreamingEndpoint(op);
|
|
459
|
+
const responseMode = inferResponseMode(op);
|
|
349
460
|
// Build method signature
|
|
350
461
|
// Use operationId directly as method name (already in desired format from OpenAPI)
|
|
351
462
|
const methodName = op.operationId;
|
|
352
463
|
const { params, isParamsOptional } = buildMethodParams(op);
|
|
353
|
-
const returnType = inferReturnType(op, isStreaming);
|
|
464
|
+
const returnType = inferReturnType(op, isStreaming, responseMode);
|
|
354
465
|
// Add JSDoc comment
|
|
355
466
|
if (op.summary) {
|
|
356
467
|
lines.push(` /** ${op.summary} */`);
|
|
@@ -416,6 +527,16 @@ function generateOperation(op) {
|
|
|
416
527
|
}
|
|
417
528
|
if (op.requestBody) {
|
|
418
529
|
requestOptions.push(`body: ${paramsPrefix}body`);
|
|
530
|
+
const requestBodyContentType = getPreferredRequestBodyContentType(op);
|
|
531
|
+
if (requestBodyContentType === 'multipart/form-data') {
|
|
532
|
+
requestOptions.push(`bodyType: 'form-data'`);
|
|
533
|
+
}
|
|
534
|
+
else if (requestBodyContentType && requestBodyContentType !== 'application/json') {
|
|
535
|
+
requestOptions.push(`bodyType: 'raw'`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!isStreaming && responseMode !== 'auto') {
|
|
539
|
+
requestOptions.push(`responseType: '${responseMode}'`);
|
|
419
540
|
}
|
|
420
541
|
if (params.includes('signal')) {
|
|
421
542
|
requestOptions.push('signal: params?.signal');
|
|
@@ -457,12 +578,18 @@ function buildMethodParams(op) {
|
|
|
457
578
|
paramFields.push(`${quotedName}${optional ? '?' : ''}: ${schemaToTypeScript(param.schema)}`);
|
|
458
579
|
}
|
|
459
580
|
if (op.requestBody) {
|
|
460
|
-
|
|
461
|
-
const bodySchema =
|
|
462
|
-
|
|
581
|
+
const requestBodyContentType = getPreferredRequestBodyContentType(op);
|
|
582
|
+
const bodySchema = (requestBodyContentType
|
|
583
|
+
? op.requestBody.content.get(requestBodyContentType)
|
|
584
|
+
: undefined)
|
|
585
|
+
|| op.requestBody.content.get('application/json')
|
|
586
|
+
|| Array.from(op.requestBody.content.values())[0];
|
|
463
587
|
if (bodySchema) {
|
|
464
588
|
const optional = !op.requestBody.required;
|
|
465
|
-
|
|
589
|
+
const bodyType = requestBodyContentType === 'multipart/form-data'
|
|
590
|
+
? 'FormData'
|
|
591
|
+
: schemaToTypeScript(bodySchema.schema);
|
|
592
|
+
paramFields.push(`body${optional ? '?' : ''}: ${bodyType}`);
|
|
466
593
|
}
|
|
467
594
|
}
|
|
468
595
|
paramFields.push('signal?: AbortSignal');
|
|
@@ -481,11 +608,17 @@ function buildMethodParams(op) {
|
|
|
481
608
|
}
|
|
482
609
|
return { params: params.join(', '), isParamsOptional };
|
|
483
610
|
}
|
|
484
|
-
function inferReturnType(op, isStreaming) {
|
|
611
|
+
function inferReturnType(op, isStreaming, responseMode) {
|
|
485
612
|
const successResponse = op.responses.get(200) || op.responses.get(201);
|
|
486
613
|
if (!successResponse?.content) {
|
|
487
614
|
return isStreaming ? 'AsyncIterable<any>' : 'Promise<void>';
|
|
488
615
|
}
|
|
616
|
+
if (responseMode === 'arrayBuffer') {
|
|
617
|
+
return isStreaming ? 'AsyncIterable<any>' : 'Promise<ArrayBuffer>';
|
|
618
|
+
}
|
|
619
|
+
if (responseMode === 'text') {
|
|
620
|
+
return isStreaming ? 'AsyncIterable<any>' : 'Promise<string>';
|
|
621
|
+
}
|
|
489
622
|
const jsonContent = successResponse.content.get('application/json');
|
|
490
623
|
if (!jsonContent) {
|
|
491
624
|
return isStreaming ? 'AsyncIterable<any>' : 'Promise<any>';
|
|
@@ -496,6 +629,46 @@ function inferReturnType(op, isStreaming) {
|
|
|
496
629
|
}
|
|
497
630
|
return `Promise<${tsType}>`;
|
|
498
631
|
}
|
|
632
|
+
function getPreferredRequestBodyContentType(op) {
|
|
633
|
+
if (!op.requestBody) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
if (op.requestBody.content.has('application/json')) {
|
|
637
|
+
return 'application/json';
|
|
638
|
+
}
|
|
639
|
+
if (op.requestBody.content.has('multipart/form-data')) {
|
|
640
|
+
return 'multipart/form-data';
|
|
641
|
+
}
|
|
642
|
+
if (op.requestBody.content.has('application/x-www-form-urlencoded')) {
|
|
643
|
+
return 'application/x-www-form-urlencoded';
|
|
644
|
+
}
|
|
645
|
+
return op.requestBody.content.keys().next().value;
|
|
646
|
+
}
|
|
647
|
+
function inferResponseMode(op) {
|
|
648
|
+
const successResponse = op.responses.get(200) || op.responses.get(201);
|
|
649
|
+
if (!successResponse?.content) {
|
|
650
|
+
return 'void';
|
|
651
|
+
}
|
|
652
|
+
for (const contentType of successResponse.content.keys()) {
|
|
653
|
+
const lowered = contentType.toLowerCase();
|
|
654
|
+
if (lowered.includes('application/json')) {
|
|
655
|
+
return 'auto';
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const contentType of successResponse.content.keys()) {
|
|
659
|
+
const lowered = contentType.toLowerCase();
|
|
660
|
+
if (lowered.startsWith('text/')) {
|
|
661
|
+
return 'text';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
for (const contentType of successResponse.content.keys()) {
|
|
665
|
+
const lowered = contentType.toLowerCase();
|
|
666
|
+
if (lowered.includes('zip') || lowered.includes('octet-stream')) {
|
|
667
|
+
return 'arrayBuffer';
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return 'auto';
|
|
671
|
+
}
|
|
499
672
|
function generateMainClient(spec) {
|
|
500
673
|
const lines = [
|
|
501
674
|
`import { ClientConfig } from './base-client.js';`,
|