@bitstack/ng-query-codegen-openapi 0.0.30 → 0.0.31-alpha.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.
@@ -1,6 +1,13 @@
1
1
  import ts from 'typescript';
2
2
  import type { GenerationOptions } from '../types';
3
3
 
4
+ /**
5
+ * 轉換類型名稱為大駝峰命名
6
+ */
7
+ const toPascalCase = (name: string): string => {
8
+ return name.charAt(0).toUpperCase() + name.slice(1);
9
+ };
10
+
4
11
  export interface EndpointInfo {
5
12
  operationName: string;
6
13
  argTypeName: string;
@@ -16,182 +23,208 @@ export interface EndpointInfo {
16
23
  }
17
24
 
18
25
  export function generateTypesFile(
19
- endpointInfos: EndpointInfo[],
26
+ endpointInfos: EndpointInfo[],
20
27
  _options: GenerationOptions,
21
28
  schemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
22
29
  operationDefinitions?: any[]
23
30
  ) {
31
+ // 創建 schema 類型名稱映射表 - 使用實際生成的類型名稱
32
+ const schemaTypeMap: Record<string, string> = {};
33
+
34
+ if (schemaInterfaces) {
35
+ Object.keys(schemaInterfaces).forEach((actualTypeName) => {
36
+ // 直接使用實際生成的類型名稱,不進行轉換
37
+ // 這確保了 types.ts 中的引用與 schema.ts 中的定義完全一致
38
+ schemaTypeMap[actualTypeName] = actualTypeName;
39
+
40
+ // 為了兼容 OpenAPI 中可能存在的不同命名方式,添加常見的映射
41
+ // 例如:TokenResponseVO -> TokenResponseVo
42
+ if (actualTypeName.endsWith('Vo')) {
43
+ const openApiName = actualTypeName.slice(0, -2) + 'VO';
44
+ schemaTypeMap[openApiName] = actualTypeName;
45
+ }
46
+ if (actualTypeName.endsWith('Dto')) {
47
+ const openApiName = actualTypeName.slice(0, -3) + 'DTO';
48
+ schemaTypeMap[openApiName] = actualTypeName;
49
+ }
50
+ });
51
+ }
24
52
 
25
53
  // 生成 import 語句
26
54
  let importStatement = `/* eslint-disable */
27
55
  // [Warning] Generated automatically - do not edit manually
28
56
 
29
57
  `;
30
-
58
+
31
59
  // 檢查是否需要引入 schema.ts
32
60
  const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
33
61
  if (hasSchemaTypes) {
34
62
  importStatement += `import * as Schema from "../schema";\n`;
35
63
  }
36
-
64
+
37
65
  importStatement += '\n';
38
-
66
+
39
67
  // 收集所有需要的類型定義
40
68
  const typeDefinitions: string[] = [];
41
-
69
+
42
70
  // 注意:不再在 types.ts 中重複生成 schema 類型
43
71
  // schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
44
-
72
+
45
73
  // 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
46
74
  const endpointTypes: string[] = [];
47
-
75
+
48
76
  // 為每個端點生成 Req/Res 類型
49
- endpointInfos.forEach(endpoint => {
77
+ endpointInfos.forEach((endpoint) => {
50
78
  // 使用 endpoint 中提供的準確類型名稱
51
79
  const reqTypeName = endpoint.argTypeName;
52
80
  const resTypeName = endpoint.responseTypeName;
53
-
81
+
54
82
  // 生成 Request 類型(總是生成)
55
83
  if (reqTypeName) {
56
- const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions);
57
- if (requestTypeContent.trim() === '' || requestTypeContent.includes('TODO') || requestTypeContent.includes('[key: string]: any')) {
84
+ const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap);
85
+ if (requestTypeContent.trim() === '') {
58
86
  // 如果沒有實際內容,使用 void
59
- endpointTypes.push(
60
- `export type ${reqTypeName} = void;`,
61
- ``
62
- );
87
+ endpointTypes.push(`export type ${reqTypeName} = void;`, ``);
63
88
  } else {
64
89
  // 有實際內容,使用 type 定義
65
- endpointTypes.push(
66
- `export type ${reqTypeName} = {`,
67
- requestTypeContent,
68
- `};`,
69
- ``
70
- );
90
+ endpointTypes.push(`export type ${reqTypeName} = {`, requestTypeContent, `};`, ``);
71
91
  }
72
92
  }
73
-
93
+
74
94
  // 生成 Response 類型(總是生成)
75
95
  if (resTypeName) {
76
- const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions);
77
- if (responseTypeContent.trim() === '' || responseTypeContent.includes('TODO') || responseTypeContent.includes('[key: string]: any')) {
96
+ const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap);
97
+ if (responseTypeContent.trim() === '') {
78
98
  // 如果沒有實際內容,使用 void
79
- endpointTypes.push(
80
- `export type ${resTypeName} = void;`,
81
- ``
82
- );
99
+ endpointTypes.push(`export type ${resTypeName} = void;`, ``);
83
100
  } else {
84
101
  // 有實際內容,使用 type 定義
85
- endpointTypes.push(
86
- `export type ${resTypeName} = {`,
87
- responseTypeContent,
88
- `};`,
89
- ``
90
- );
102
+ endpointTypes.push(`export type ${resTypeName} = {`, responseTypeContent, `};`, ``);
91
103
  }
92
104
  }
93
105
  });
94
-
106
+
95
107
  if (endpointTypes.length > 0) {
96
108
  typeDefinitions.push(endpointTypes.join('\n'));
97
109
  }
98
-
110
+
99
111
  // 如果沒有任何類型定義,至少添加一些基本說明
100
112
  if (typeDefinitions.length === 0) {
101
- typeDefinitions.push(
102
- `// 此檔案用於定義 API 相關的類型`,
103
- `// 類型定義會根據 OpenAPI Schema 自動生成`,
104
- ``
105
- );
113
+ typeDefinitions.push(`// 此檔案用於定義 API 相關的類型`, `// 類型定義會根據 OpenAPI Schema 自動生成`, ``);
106
114
  }
107
-
115
+
108
116
  return importStatement + typeDefinitions.join('\n\n');
109
117
  }
110
118
 
111
119
  /**
112
120
  * 生成 Request 類型的內容
113
121
  */
114
- function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[]): string {
122
+ function generateRequestTypeContent(
123
+ endpoint: EndpointInfo,
124
+ operationDefinitions?: any[],
125
+ schemaTypeMap: Record<string, string> = {}
126
+ ): string {
115
127
  const properties: string[] = [];
116
-
128
+
117
129
  // 如果有 query 參數
118
130
  if (endpoint.queryParams && endpoint.queryParams.length > 0) {
119
- endpoint.queryParams.forEach(param => {
131
+ endpoint.queryParams.forEach((param) => {
120
132
  const optional = param.required ? '' : '?';
121
- const paramType = getTypeFromParameter(param);
133
+ const paramType = getTypeFromParameter(param, schemaTypeMap);
122
134
  properties.push(` ${param.name}${optional}: ${paramType};`);
123
135
  });
124
136
  }
125
-
137
+
126
138
  // 如果有 path 參數
127
139
  if (endpoint.pathParams && endpoint.pathParams.length > 0) {
128
- endpoint.pathParams.forEach(param => {
140
+ endpoint.pathParams.forEach((param) => {
129
141
  const optional = param.required ? '' : '?';
130
- const paramType = getTypeFromParameter(param);
142
+ const paramType = getTypeFromParameter(param, schemaTypeMap);
131
143
  properties.push(` ${param.name}${optional}: ${paramType};`);
132
144
  });
133
145
  }
134
-
146
+
135
147
  // 如果有 request body(從 operationDefinitions 中獲取)
136
- const operationDef = operationDefinitions?.find(op => {
148
+ const operationDef = operationDefinitions?.find((op) => {
137
149
  // 嘗試多種匹配方式
138
- return op.operation?.operationId === endpoint.operationName ||
139
- op.operation?.operationId === endpoint.operationName.toLowerCase() ||
140
- // 也嘗試匹配 verb + path 組合
141
- (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
150
+ return (
151
+ op.operation?.operationId === endpoint.operationName ||
152
+ op.operation?.operationId === endpoint.operationName.toLowerCase() ||
153
+ // 也嘗試匹配 verb + path 組合
154
+ (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path)
155
+ );
142
156
  });
143
-
157
+
144
158
  if (operationDef?.operation?.requestBody) {
145
159
  const requestBody = operationDef.operation.requestBody;
146
160
  const content = requestBody.content;
147
-
148
- // 處理不同的 content types
149
- const jsonContent = content['application/json'];
161
+
162
+ // 處理不同的 content types,優先使用 application/json,其次嘗試其他類型
163
+ const jsonContent = content['application/json'] || content['*/*'];
150
164
  const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
151
-
165
+
152
166
  if (jsonContent?.schema) {
153
- const bodyType = getTypeFromSchema(jsonContent.schema);
167
+ // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
168
+ const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1);
154
169
  properties.push(` body: ${bodyType};`);
155
170
  } else if (formContent?.schema) {
156
- const bodyType = getTypeFromSchema(formContent.schema);
171
+ // indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
172
+ const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1);
157
173
  properties.push(` body: ${bodyType};`);
158
174
  } else {
159
- properties.push(` body?: any; // Request body from OpenAPI`);
175
+ // fallback 到第一個可用的 content-type
176
+ const firstContent = Object.values(content)[0] as any;
177
+ if (firstContent?.schema) {
178
+ const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1);
179
+ properties.push(` body: ${bodyType};`);
180
+ } else {
181
+ properties.push(` body?: any; // Request body from OpenAPI`);
182
+ }
160
183
  }
161
184
  }
162
-
185
+
163
186
  // 如果沒有任何參數,返回空內容(將由調用方處理為 void)
164
187
  if (properties.length === 0) {
165
188
  return ''; // 返回空字串,讓調用方決定使用 void
166
189
  }
167
-
190
+
168
191
  return properties.join('\n');
169
192
  }
170
193
 
171
194
  /**
172
195
  * 生成 Response 類型的內容
173
196
  */
174
- function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[]): string {
197
+ function generateResponseTypeContent(
198
+ endpoint: EndpointInfo,
199
+ operationDefinitions?: any[],
200
+ schemaTypeMap: Record<string, string> = {}
201
+ ): string {
175
202
  const properties: string[] = [];
176
-
203
+
177
204
  // 嘗試從 operationDefinitions 中獲取響應結構
178
- const operationDef = operationDefinitions?.find(op => {
205
+ const operationDef = operationDefinitions?.find((op) => {
179
206
  // 嘗試多種匹配方式
180
- return op.operation?.operationId === endpoint.operationName ||
181
- op.operation?.operationId === endpoint.operationName.toLowerCase() ||
182
- // 也嘗試匹配 verb + path 組合
183
- (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
207
+ return (
208
+ op.operation?.operationId === endpoint.operationName ||
209
+ op.operation?.operationId === endpoint.operationName.toLowerCase() ||
210
+ // 也嘗試匹配 verb + path 組合
211
+ (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path)
212
+ );
184
213
  });
185
-
214
+
186
215
  if (operationDef?.operation?.responses) {
187
216
  // 檢查 200 響應
188
- const successResponse = operationDef.operation.responses['200'] ||
189
- operationDef.operation.responses['201'];
190
-
217
+ const successResponse = operationDef.operation.responses['200'] || operationDef.operation.responses['201'];
218
+
191
219
  if (successResponse?.content) {
192
- const jsonContent = successResponse.content['application/json'];
220
+ // 優先使用 application/json,其次嘗試其他 content-type(包括 */*)
221
+ const jsonContent =
222
+ successResponse.content['application/json'] ||
223
+ successResponse.content['*/*'] ||
224
+ Object.values(successResponse.content)[0]; // fallback 到第一個可用的 content-type
225
+
193
226
  if (jsonContent?.schema) {
194
- const responseProps = parseSchemaProperties(jsonContent.schema);
227
+ const responseProps = parseSchemaProperties(jsonContent.schema, schemaTypeMap);
195
228
  properties.push(...responseProps);
196
229
  } else {
197
230
  properties.push(` // Success response from OpenAPI`);
@@ -199,87 +232,158 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
199
232
  }
200
233
  }
201
234
  }
202
-
235
+
203
236
  // 如果沒有響應定義,返回空內容(將由調用方處理為 void)
204
237
  if (properties.length === 0) {
205
238
  return ''; // 返回空字串,讓調用方決定使用 void
206
239
  }
207
-
240
+
208
241
  return properties.join('\n');
209
242
  }
210
243
 
211
244
  /**
212
245
  * 解析 OpenAPI schema 的 properties 並生成 TypeScript 屬性定義
213
246
  */
214
- function parseSchemaProperties(schema: any): string[] {
247
+ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}): string[] {
215
248
  const properties: string[] = [];
216
-
249
+
217
250
  if (schema.type === 'object' && schema.properties) {
218
251
  const required = schema.required || [];
219
-
252
+
220
253
  Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => {
221
254
  const isRequired = required.includes(propName);
222
255
  const optional = isRequired ? '' : '?';
223
- const propType = getTypeFromSchema(propSchema);
224
- const description = propSchema.description ? ` // ${propSchema.description}` : '';
225
-
226
- properties.push(` ${propName}${optional}: ${propType};${description}`);
256
+ // indentLevel=1 因為屬性已經在類型定義內(有 2 個空格縮排)
257
+ const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1);
258
+
259
+ // 如果屬性名包含特殊字符(如 -),需要加上引號
260
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(propName);
261
+ const quotedPropName = needsQuotes ? `"${propName}"` : propName;
262
+
263
+ // 生成 JSDoc 註解
264
+ if (propSchema.description) {
265
+ properties.push(` /** ${propSchema.description} */`);
266
+ }
267
+ properties.push(` ${quotedPropName}${optional}: ${propType};`);
227
268
  });
228
269
  }
229
-
270
+
230
271
  return properties;
231
272
  }
232
273
 
233
274
  /**
234
275
  * 從 OpenAPI schema 獲取 TypeScript 類型
276
+ * @param schema OpenAPI schema 定義
277
+ * @param schemaTypeMap 類型名稱映射表
278
+ * @param indentLevel 縮排層級,用於格式化內嵌物件
235
279
  */
236
- function getTypeFromSchema(schema: any): string {
280
+ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0): string {
237
281
  if (!schema) return 'any';
238
-
282
+
239
283
  // 處理 $ref 引用,使用 Schema.TypeName 格式
240
284
  if (schema.$ref) {
241
285
  const refPath = schema.$ref;
242
286
  if (refPath.startsWith('#/components/schemas/')) {
243
- const typeName = refPath.replace('#/components/schemas/', '');
244
- return `Schema.${typeName}`;
287
+ const originalTypeName = refPath.replace('#/components/schemas/', '');
288
+ // 使用映射表查找實際的類型名稱,並轉換為大駝峰
289
+ const actualTypeName = schemaTypeMap[originalTypeName] || originalTypeName;
290
+ const pascalCaseTypeName = toPascalCase(actualTypeName);
291
+ const baseType = `Schema.${pascalCaseTypeName}`;
292
+ // 處理 nullable
293
+ return schema.nullable ? `${baseType} | null` : baseType;
245
294
  }
246
295
  }
247
-
296
+
297
+ let baseType: string;
298
+
248
299
  switch (schema.type) {
249
300
  case 'string':
250
301
  if (schema.enum) {
251
- return schema.enum.map((val: string) => `"${val}"`).join(' | ');
302
+ baseType = schema.enum.map((val: string) => `"${val}"`).join(' | ');
303
+ } else if (schema.format === 'binary') {
304
+ // 處理檔案上傳:format: "binary" 應該對應 Blob 類型
305
+ baseType = 'Blob';
306
+ } else {
307
+ baseType = 'string';
252
308
  }
253
- return 'string';
309
+ break;
254
310
  case 'number':
255
311
  case 'integer':
256
- return 'number';
312
+ baseType = 'number';
313
+ break;
257
314
  case 'boolean':
258
- return 'boolean';
315
+ baseType = 'boolean';
316
+ break;
259
317
  case 'array':
260
- const itemType = schema.items ? getTypeFromSchema(schema.items) : 'any';
261
- return `${itemType}[]`;
318
+ const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel) : 'any';
319
+ // 如果 itemType 包含聯合類型(包含 |),需要加括號
320
+ const needsParentheses = itemType.includes('|');
321
+ baseType = needsParentheses ? `(${itemType})[]` : `${itemType}[]`;
322
+ break;
262
323
  case 'object':
263
324
  if (schema.properties) {
264
- // 如果有具體的屬性定義,生成內聯對象類型
265
- const props = Object.entries(schema.properties).map(([key, propSchema]: [string, any]) => {
266
- const required = schema.required || [];
267
- const optional = required.includes(key) ? '' : '?';
268
- const type = getTypeFromSchema(propSchema);
269
- return `${key}${optional}: ${type}`;
270
- }).join('; ');
271
- return `{ ${props} }`;
325
+ // 如果有具體的屬性定義,生成內聯對象類型(多行格式)
326
+ const entries = Object.entries(schema.properties);
327
+
328
+ // 如果沒有屬性但有 additionalProperties,生成 Record 類型
329
+ if (entries.length === 0) {
330
+ if (schema.additionalProperties) {
331
+ const valueType =
332
+ schema.additionalProperties === true
333
+ ? 'any'
334
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
335
+ baseType = `Record<string, ${valueType}>`;
336
+ } else {
337
+ baseType = '{}';
338
+ }
339
+ } else {
340
+ // 計算下一層的縮排
341
+ const nextIndent = ' '.repeat(indentLevel + 1);
342
+ const currentIndent = ' '.repeat(indentLevel);
343
+
344
+ const props: string[] = [];
345
+ entries.forEach(([key, propSchema]: [string, any]) => {
346
+ const required = schema.required || [];
347
+ const optional = required.includes(key) ? '' : '?';
348
+ const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1);
349
+
350
+ // 如果屬性名包含特殊字符(如 -),需要加上引號
351
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
352
+ const quotedKey = needsQuotes ? `"${key}"` : key;
353
+
354
+ // 生成 JSDoc 註解
355
+ if (propSchema.description) {
356
+ props.push(`${nextIndent}/** ${propSchema.description} */`);
357
+ }
358
+ props.push(`${nextIndent}${quotedKey}${optional}: ${type};`);
359
+ });
360
+
361
+ baseType = `{\n${props.join('\n')}\n${currentIndent}}`;
362
+ }
363
+ } else if (schema.additionalProperties) {
364
+ // 如果沒有 properties 但有 additionalProperties
365
+ const valueType =
366
+ schema.additionalProperties === true
367
+ ? 'any'
368
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
369
+ baseType = `Record<string, ${valueType}>`;
370
+ } else {
371
+ baseType = 'any';
272
372
  }
273
- return 'any';
373
+ break;
274
374
  default:
275
- return 'any';
375
+ baseType = 'any';
376
+ break;
276
377
  }
378
+
379
+ // 處理 nullable
380
+ return schema.nullable ? `${baseType} | null` : baseType;
277
381
  }
278
382
 
279
383
  /**
280
384
  * 從參數定義中獲取 TypeScript 類型
281
385
  */
282
- function getTypeFromParameter(param: any): string {
386
+ function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}): string {
283
387
  if (!param.schema) return 'any';
284
- return getTypeFromSchema(param.schema);
285
- }
388
+ return getTypeFromSchema(param.schema, schemaTypeMap);
389
+ }
@@ -1,12 +1,10 @@
1
1
  import ts from 'typescript';
2
2
 
3
-
4
3
  /**
5
4
  * 產生 Utils function 內容
6
5
  * @param interfaces
7
6
  */
8
7
  export function generateUtilsFile() {
9
-
10
8
  // 分析接口內容以找出需要從 shared-types 導入的類型
11
9
  return `/* eslint-disable */
12
10
  // [Warning] Generated automatically - do not edit manually
@@ -21,7 +19,7 @@ export function withoutUndefined(obj?: Record<string, any>) {
21
19
  );
22
20
  }
23
21
 
24
- `
22
+ `;
25
23
  }
26
24
 
27
25
  /**
@@ -30,4 +28,4 @@ export function withoutUndefined(obj?: Record<string, any>) {
30
28
  */
31
29
  export function toCamelCase(str: string): string {
32
30
  return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
33
- }
31
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { UnifiedCodeGenerator, type UnifiedGenerationOptions, type UnifiedGenerationResult } from './services/unified-code-generator';
1
+ import {
2
+ UnifiedCodeGenerator,
3
+ type UnifiedGenerationOptions,
4
+ type UnifiedGenerationResult,
5
+ } from './services/unified-code-generator';
2
6
 
3
7
  export type { OutputFilesConfig, ConfigFile } from './types';
4
8
 
5
-
6
9
  /**
7
10
  * 產生 Endpoints - 直接使用統一代碼生成器
8
11
  * @param options - 端點生成選項
@@ -11,7 +14,7 @@ export async function generateEndpoints(options: UnifiedGenerationOptions): Prom
11
14
  const generator = new UnifiedCodeGenerator(options);
12
15
 
13
16
  const result = await generator.generateAll();
14
-
17
+
15
18
  if (!result.success) {
16
19
  if (result.errors.length > 0) {
17
20
  throw result.errors[0];