@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 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.INIT_CWD || process.cwd();
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
- props.push(` ${propName}${optional ? '?' : ''}: ${propType};`);
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
- baseType = schema.items ? `${schemaToTypeScript(schema.items)}[]` : 'any[]';
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
- props.push(`${propName}${optional ? '?' : ''}: ${schemaToTypeScript(propSchema)}`);
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
- // Add content type for body requests
159
- if (options?.body && !headers['Content-Type']) {
160
- headers['Content-Type'] = 'application/json';
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: options?.body ? JSON.stringify(options.body) : undefined,
293
+ body: requestBody,
167
294
  signal: options?.signal,
168
295
  credentials: this.config.credentials,
169
296
  });
170
297
 
171
298
  if (!response.ok) {
172
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
299
+ const errorBody = await this.parseResponseBody(response, 'auto');
300
+ throw new ApiError(response, errorBody);
173
301
  }
174
302
 
175
- // Handle no content responses
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
- throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
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 bodySchema = op.requestBody.content.get('application/json');
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
- paramFields.push(`body${optional ? '?' : ''}: ${schemaToTypeScript(bodySchema.schema)}`);
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
@@ -26,7 +26,7 @@ export interface Operation {
26
26
  }
27
27
  export interface Parameter {
28
28
  name: string;
29
- in: 'path' | 'query' | 'header';
29
+ in: 'path' | 'query' | 'header' | 'cookie';
30
30
  required: boolean;
31
31
  schema: SchemaInfo;
32
32
  description?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taico/openapi-sdkgen",
3
- "version": "0.1.1",
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.0.0",
40
- "@nestjs/core": "^11.0.0",
41
- "@nestjs/platform-express": "^11.0.0",
42
- "@nestjs/swagger": "^11.2.1",
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",