@taico/openapi-sdkgen 0.1.1 → 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/cli.js +3 -1
- package/dist/generator.js +209 -25
- package/dist/parser.js +8 -1
- package/dist/types.d.ts +1 -1
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,9 @@ if (args.length < 2) {
|
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
9
|
const [inputPath, outputDir] = args;
|
|
10
|
-
const invocationDir = process.env.
|
|
10
|
+
const invocationDir = process.env.npm_lifecycle_event
|
|
11
|
+
? process.cwd()
|
|
12
|
+
: (process.env.INIT_CWD || process.cwd());
|
|
11
13
|
const resolvedInputPath = resolve(invocationDir, inputPath);
|
|
12
14
|
const resolvedOutputDir = resolve(invocationDir, outputDir);
|
|
13
15
|
generate({ inputPath: resolvedInputPath, outputDir: resolvedOutputDir })
|
package/dist/generator.js
CHANGED
|
@@ -32,7 +32,8 @@ function generateTypeDefinition(name, schema) {
|
|
|
32
32
|
for (const [propName, propSchema] of schema.properties) {
|
|
33
33
|
const optional = !schema.required?.includes(propName);
|
|
34
34
|
const propType = schemaToTypeScript(propSchema);
|
|
35
|
-
|
|
35
|
+
const quotedPropName = quoteIfNeeded(propName);
|
|
36
|
+
props.push(` ${quotedPropName}${optional ? '?' : ''}: ${propType};`);
|
|
36
37
|
}
|
|
37
38
|
return `export interface ${name} {\n${props.join('\n')}\n}`;
|
|
38
39
|
}
|
|
@@ -52,7 +53,7 @@ function schemaToTypeScript(schema) {
|
|
|
52
53
|
let baseType;
|
|
53
54
|
switch (schema.type) {
|
|
54
55
|
case 'string':
|
|
55
|
-
baseType = 'string';
|
|
56
|
+
baseType = schema.format === 'binary' ? 'Blob' : 'string';
|
|
56
57
|
break;
|
|
57
58
|
case 'number':
|
|
58
59
|
case 'integer':
|
|
@@ -62,14 +63,22 @@ function schemaToTypeScript(schema) {
|
|
|
62
63
|
baseType = 'boolean';
|
|
63
64
|
break;
|
|
64
65
|
case 'array':
|
|
65
|
-
|
|
66
|
+
if (schema.items) {
|
|
67
|
+
const itemType = schemaToTypeScript(schema.items);
|
|
68
|
+
// Wrap in parentheses if the item type contains a union (|) to ensure correct precedence
|
|
69
|
+
baseType = itemType.includes(' | ') ? `(${itemType})[]` : `${itemType}[]`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
baseType = 'any[]';
|
|
73
|
+
}
|
|
66
74
|
break;
|
|
67
75
|
case 'object':
|
|
68
76
|
if (schema.properties) {
|
|
69
77
|
const props = [];
|
|
70
78
|
for (const [propName, propSchema] of schema.properties) {
|
|
71
79
|
const optional = !schema.required?.includes(propName);
|
|
72
|
-
|
|
80
|
+
const quotedPropName = quoteIfNeeded(propName);
|
|
81
|
+
props.push(`${quotedPropName}${optional ? '?' : ''}: ${schemaToTypeScript(propSchema)}`);
|
|
73
82
|
}
|
|
74
83
|
baseType = `{ ${props.join('; ')} }`;
|
|
75
84
|
}
|
|
@@ -95,9 +104,113 @@ export interface ClientConfig {
|
|
|
95
104
|
headers?: Record<string, string>;
|
|
96
105
|
}
|
|
97
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
|
+
|
|
98
154
|
export class BaseClient {
|
|
99
155
|
constructor(protected config: ClientConfig) {}
|
|
100
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
|
+
|
|
101
214
|
private async buildAuthHeaders(): Promise<Record<string, string>> {
|
|
102
215
|
const headers: Record<string, string> = {};
|
|
103
216
|
if (this.config.getAccessToken) {
|
|
@@ -116,6 +229,8 @@ export class BaseClient {
|
|
|
116
229
|
options?: {
|
|
117
230
|
params?: Record<string, any>;
|
|
118
231
|
body?: any;
|
|
232
|
+
bodyType?: 'json' | 'form-data' | 'raw';
|
|
233
|
+
responseType?: 'auto' | 'json' | 'text' | 'arrayBuffer' | 'void';
|
|
119
234
|
headers?: Record<string, string>;
|
|
120
235
|
signal?: AbortSignal;
|
|
121
236
|
}
|
|
@@ -155,34 +270,37 @@ export class BaseClient {
|
|
|
155
270
|
// Add auth headers
|
|
156
271
|
Object.assign(headers, await this.buildAuthHeaders());
|
|
157
272
|
|
|
158
|
-
|
|
159
|
-
if (options?.body
|
|
160
|
-
|
|
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
|
+
}
|
|
161
288
|
}
|
|
162
289
|
|
|
163
290
|
const response = await fetch(url.toString(), {
|
|
164
291
|
method,
|
|
165
292
|
headers,
|
|
166
|
-
body:
|
|
293
|
+
body: requestBody,
|
|
167
294
|
signal: options?.signal,
|
|
168
295
|
credentials: this.config.credentials,
|
|
169
296
|
});
|
|
170
297
|
|
|
171
298
|
if (!response.ok) {
|
|
172
|
-
|
|
299
|
+
const errorBody = await this.parseResponseBody(response, 'auto');
|
|
300
|
+
throw new ApiError(response, errorBody);
|
|
173
301
|
}
|
|
174
302
|
|
|
175
|
-
|
|
176
|
-
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
177
|
-
return undefined as T;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const contentType = response.headers.get('content-type');
|
|
181
|
-
if (contentType?.includes('application/json')) {
|
|
182
|
-
return response.json() as Promise<T>;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return response.text() as any;
|
|
303
|
+
return await this.parseResponseBody(response, options?.responseType ?? 'auto') as T;
|
|
186
304
|
}
|
|
187
305
|
|
|
188
306
|
protected async *streamEvents<T = any>(
|
|
@@ -237,7 +355,8 @@ export class BaseClient {
|
|
|
237
355
|
});
|
|
238
356
|
|
|
239
357
|
if (!response.ok) {
|
|
240
|
-
|
|
358
|
+
const errorBody = await this.parseResponseBody(response);
|
|
359
|
+
throw new ApiError(response, errorBody);
|
|
241
360
|
}
|
|
242
361
|
|
|
243
362
|
if (!response.body) {
|
|
@@ -337,11 +456,12 @@ function generateOperation(op) {
|
|
|
337
456
|
const lines = [];
|
|
338
457
|
// Determine if this is a streaming endpoint by checking response content-type
|
|
339
458
|
const isStreaming = isStreamingEndpoint(op);
|
|
459
|
+
const responseMode = inferResponseMode(op);
|
|
340
460
|
// Build method signature
|
|
341
461
|
// Use operationId directly as method name (already in desired format from OpenAPI)
|
|
342
462
|
const methodName = op.operationId;
|
|
343
463
|
const { params, isParamsOptional } = buildMethodParams(op);
|
|
344
|
-
const returnType = inferReturnType(op, isStreaming);
|
|
464
|
+
const returnType = inferReturnType(op, isStreaming, responseMode);
|
|
345
465
|
// Add JSDoc comment
|
|
346
466
|
if (op.summary) {
|
|
347
467
|
lines.push(` /** ${op.summary} */`);
|
|
@@ -407,6 +527,16 @@ function generateOperation(op) {
|
|
|
407
527
|
}
|
|
408
528
|
if (op.requestBody) {
|
|
409
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}'`);
|
|
410
540
|
}
|
|
411
541
|
if (params.includes('signal')) {
|
|
412
542
|
requestOptions.push('signal: params?.signal');
|
|
@@ -448,10 +578,18 @@ function buildMethodParams(op) {
|
|
|
448
578
|
paramFields.push(`${quotedName}${optional ? '?' : ''}: ${schemaToTypeScript(param.schema)}`);
|
|
449
579
|
}
|
|
450
580
|
if (op.requestBody) {
|
|
451
|
-
const
|
|
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];
|
|
452
587
|
if (bodySchema) {
|
|
453
588
|
const optional = !op.requestBody.required;
|
|
454
|
-
|
|
589
|
+
const bodyType = requestBodyContentType === 'multipart/form-data'
|
|
590
|
+
? 'FormData'
|
|
591
|
+
: schemaToTypeScript(bodySchema.schema);
|
|
592
|
+
paramFields.push(`body${optional ? '?' : ''}: ${bodyType}`);
|
|
455
593
|
}
|
|
456
594
|
}
|
|
457
595
|
paramFields.push('signal?: AbortSignal');
|
|
@@ -470,11 +608,17 @@ function buildMethodParams(op) {
|
|
|
470
608
|
}
|
|
471
609
|
return { params: params.join(', '), isParamsOptional };
|
|
472
610
|
}
|
|
473
|
-
function inferReturnType(op, isStreaming) {
|
|
611
|
+
function inferReturnType(op, isStreaming, responseMode) {
|
|
474
612
|
const successResponse = op.responses.get(200) || op.responses.get(201);
|
|
475
613
|
if (!successResponse?.content) {
|
|
476
614
|
return isStreaming ? 'AsyncIterable<any>' : 'Promise<void>';
|
|
477
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
|
+
}
|
|
478
622
|
const jsonContent = successResponse.content.get('application/json');
|
|
479
623
|
if (!jsonContent) {
|
|
480
624
|
return isStreaming ? 'AsyncIterable<any>' : 'Promise<any>';
|
|
@@ -485,6 +629,46 @@ function inferReturnType(op, isStreaming) {
|
|
|
485
629
|
}
|
|
486
630
|
return `Promise<${tsType}>`;
|
|
487
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
|
+
}
|
|
488
672
|
function generateMainClient(spec) {
|
|
489
673
|
const lines = [
|
|
490
674
|
`import { ClientConfig } from './base-client.js';`,
|
package/dist/parser.js
CHANGED
|
@@ -30,7 +30,7 @@ function parseOperations(paths) {
|
|
|
30
30
|
// Parse path-level parameters (common to all operations on this path)
|
|
31
31
|
const pathLevelParams = parseParameters(pathItem.parameters || []);
|
|
32
32
|
for (const [method, operation] of Object.entries(pathItem)) {
|
|
33
|
-
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
|
|
33
|
+
if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) {
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
36
|
const op = operation;
|
|
@@ -119,6 +119,13 @@ function parseSchema(schema) {
|
|
|
119
119
|
if (!schema) {
|
|
120
120
|
return { type: 'any' };
|
|
121
121
|
}
|
|
122
|
+
if (schema.allOf?.length === 1) {
|
|
123
|
+
const merged = parseSchema(schema.allOf[0]);
|
|
124
|
+
return {
|
|
125
|
+
...merged,
|
|
126
|
+
nullable: schema.nullable ?? merged.nullable,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
122
129
|
if (schema.$ref) {
|
|
123
130
|
return {
|
|
124
131
|
type: 'ref',
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taico/openapi-sdkgen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"release": "npm run build && npm publish --access public"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@nestjs/common": "^11.
|
|
40
|
-
"@nestjs/core": "^11.
|
|
41
|
-
"@nestjs/platform-express": "^11.
|
|
42
|
-
"@nestjs/swagger": "^11.2.
|
|
39
|
+
"@nestjs/common": "^11.1.17",
|
|
40
|
+
"@nestjs/core": "^11.1.17",
|
|
41
|
+
"@nestjs/platform-express": "^11.1.17",
|
|
42
|
+
"@nestjs/swagger": "^11.2.6",
|
|
43
43
|
"@types/node": "^20.0.0",
|
|
44
44
|
"reflect-metadata": "^0.2.0",
|
|
45
45
|
"rxjs": "^7.8.1",
|