@acrool/rtk-query-codegen-openapi 1.1.1 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acrool/rtk-query-codegen-openapi",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "module": "lib/index.mjs",
@@ -14,7 +14,7 @@ export function generateRtkEnhanceEndpointsFile(endpointInfos: Array<{
14
14
  summary: string;
15
15
  }>, options: GenerationOptions) {
16
16
 
17
- const { groupKey } = options;
17
+ const { groupKey, cacheTagTypes } = options;
18
18
 
19
19
  // 生成端點配置
20
20
  const endpointConfigs = endpointInfos.map(info => {
@@ -32,12 +32,20 @@ export function generateRtkEnhanceEndpointsFile(endpointInfos: Array<{
32
32
  }
33
33
  }).join('\n');
34
34
 
35
- return `/* eslint-disable */
35
+ // 生成 import 語句
36
+ let importStatements = `/* eslint-disable */
36
37
  // [Warning] Generated automatically - do not edit manually
37
38
 
38
- import { ECacheTagTypes } from "@/store/tagTypes";
39
- import api from "./query.generated";
39
+ `;
40
+
41
+ // 如果有 cacheTagTypes 配置,則添加導入
42
+ if (cacheTagTypes) {
43
+ importStatements += `import { ${cacheTagTypes.importReturnTypeName} } from "${cacheTagTypes.file}";\n`;
44
+ }
45
+
46
+ importStatements += `import api from "./query.generated";\n`;
40
47
 
48
+ return `${importStatements}
41
49
  const enhancedApi = api.enhanceEndpoints({
42
50
  endpoints: {
43
51
  ${endpointConfigs}
@@ -46,4 +54,4 @@ ${endpointConfigs}
46
54
 
47
55
  export default enhancedApi;
48
56
  `;
49
- }
57
+ }
@@ -86,7 +86,7 @@ ${paramsLines}
86
86
  `;
87
87
 
88
88
  const httpClientImport = options.httpClient
89
- ? `import {${options.httpClient.importReturnTypeName || options.httpClient.importName}} from "${options.httpClient.file}";
89
+ ? `import {${options.httpClient.importReturnTypeName}} from "${options.httpClient.file}";
90
90
  `
91
91
  : `import {IRestFulEndpointsQueryReturn} from "@acrool/react-fetcher";
92
92
  `;
@@ -126,4 +126,4 @@ ${endpointInfos.map(info => {
126
126
 
127
127
  export default injectedRtkApi;
128
128
  `;
129
- }
129
+ }
@@ -56,30 +56,30 @@ export function generateTypesFile(
56
56
  // [Warning] Generated automatically - do not edit manually
57
57
 
58
58
  `;
59
-
59
+
60
60
  // 檢查是否需要引入 schema.ts
61
61
  const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
62
62
  if (hasSchemaTypes) {
63
63
  importStatement += `import * as Schema from "../schema";\n`;
64
64
  }
65
-
65
+
66
66
  importStatement += '\n';
67
-
67
+
68
68
  // 收集所有需要的類型定義
69
69
  const typeDefinitions: string[] = [];
70
-
70
+
71
71
  // 注意:不再在 types.ts 中重複生成 schema 類型
72
72
  // schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
73
-
73
+
74
74
  // 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
75
75
  const endpointTypes: string[] = [];
76
-
76
+
77
77
  // 為每個端點生成 Req/Res 類型
78
78
  endpointInfos.forEach(endpoint => {
79
79
  // 使用 endpoint 中提供的準確類型名稱
80
80
  const reqTypeName = endpoint.argTypeName;
81
81
  const resTypeName = endpoint.responseTypeName;
82
-
82
+
83
83
  // 生成 Request 類型(總是生成)
84
84
  if (reqTypeName) {
85
85
  const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap);
@@ -99,7 +99,7 @@ export function generateTypesFile(
99
99
  );
100
100
  }
101
101
  }
102
-
102
+
103
103
  // 生成 Response 類型(總是生成)
104
104
  if (resTypeName) {
105
105
  const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap);
@@ -120,11 +120,11 @@ export function generateTypesFile(
120
120
  }
121
121
  }
122
122
  });
123
-
123
+
124
124
  if (endpointTypes.length > 0) {
125
125
  typeDefinitions.push(endpointTypes.join('\n'));
126
126
  }
127
-
127
+
128
128
  // 如果沒有任何類型定義,至少添加一些基本說明
129
129
  if (typeDefinitions.length === 0) {
130
130
  typeDefinitions.push(
@@ -133,7 +133,7 @@ export function generateTypesFile(
133
133
  ``
134
134
  );
135
135
  }
136
-
136
+
137
137
  return importStatement + typeDefinitions.join('\n\n');
138
138
  }
139
139
 
@@ -142,7 +142,7 @@ export function generateTypesFile(
142
142
  */
143
143
  function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): string {
144
144
  const properties: string[] = [];
145
-
145
+
146
146
  // 如果有 query 參數
147
147
  if (endpoint.queryParams && endpoint.queryParams.length > 0) {
148
148
  endpoint.queryParams.forEach(param => {
@@ -151,7 +151,7 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
151
151
  properties.push(` ${param.name}${optional}: ${paramType};`);
152
152
  });
153
153
  }
154
-
154
+
155
155
  // 如果有 path 參數
156
156
  if (endpoint.pathParams && endpoint.pathParams.length > 0) {
157
157
  endpoint.pathParams.forEach(param => {
@@ -160,22 +160,22 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
160
160
  properties.push(` ${param.name}${optional}: ${paramType};`);
161
161
  });
162
162
  }
163
-
163
+
164
164
  // 如果有 request body(從 operationDefinitions 中獲取)
165
165
  const operationDef = operationDefinitions?.find(op => {
166
166
  // 嘗試多種匹配方式
167
167
  return op.operation?.operationId === endpoint.operationName ||
168
- op.operation?.operationId === endpoint.operationName.toLowerCase() ||
169
- // 也嘗試匹配 verb + path 組合
170
- (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
168
+ op.operation?.operationId === endpoint.operationName.toLowerCase() ||
169
+ // 也嘗試匹配 verb + path 組合
170
+ (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
171
171
  });
172
-
172
+
173
173
  if (operationDef?.operation?.requestBody) {
174
174
  const requestBody = operationDef.operation.requestBody;
175
175
  const content = requestBody.content;
176
176
 
177
- // 處理不同的 content types
178
- const jsonContent = content['application/json'];
177
+ // 處理不同的 content types,優先使用 application/json,其次嘗試其他類型
178
+ const jsonContent = content['application/json'] || content['*/*'];
179
179
  const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
180
180
 
181
181
  if (jsonContent?.schema) {
@@ -187,15 +187,22 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
187
187
  const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1);
188
188
  properties.push(` body: ${bodyType};`);
189
189
  } else {
190
- properties.push(` body?: any; // Request body from OpenAPI`);
190
+ // fallback 到第一個可用的 content-type
191
+ const firstContent = Object.values(content)[0] as any;
192
+ if (firstContent?.schema) {
193
+ const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1);
194
+ properties.push(` body: ${bodyType};`);
195
+ } else {
196
+ properties.push(` body?: any; // Request body from OpenAPI`);
197
+ }
191
198
  }
192
199
  }
193
-
200
+
194
201
  // 如果沒有任何參數,返回空內容(將由調用方處理為 void)
195
202
  if (properties.length === 0) {
196
203
  return ''; // 返回空字串,讓調用方決定使用 void
197
204
  }
198
-
205
+
199
206
  return properties.join('\n');
200
207
  }
201
208
 
@@ -204,23 +211,27 @@ function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions
204
211
  */
205
212
  function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[], schemaTypeMap: Record<string, string> = {}): string {
206
213
  const properties: string[] = [];
207
-
214
+
208
215
  // 嘗試從 operationDefinitions 中獲取響應結構
209
216
  const operationDef = operationDefinitions?.find(op => {
210
217
  // 嘗試多種匹配方式
211
218
  return op.operation?.operationId === endpoint.operationName ||
212
- op.operation?.operationId === endpoint.operationName.toLowerCase() ||
213
- // 也嘗試匹配 verb + path 組合
214
- (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
219
+ op.operation?.operationId === endpoint.operationName.toLowerCase() ||
220
+ // 也嘗試匹配 verb + path 組合
221
+ (op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
215
222
  });
216
-
223
+
217
224
  if (operationDef?.operation?.responses) {
218
225
  // 檢查 200 響應
219
- const successResponse = operationDef.operation.responses['200'] ||
220
- operationDef.operation.responses['201'];
221
-
226
+ const successResponse = operationDef.operation.responses['200'] ||
227
+ operationDef.operation.responses['201'];
228
+
222
229
  if (successResponse?.content) {
223
- const jsonContent = successResponse.content['application/json'];
230
+ // 優先使用 application/json,其次嘗試其他 content-type(包括 */*)
231
+ const jsonContent = successResponse.content['application/json'] ||
232
+ successResponse.content['*/*'] ||
233
+ Object.values(successResponse.content)[0]; // fallback 到第一個可用的 content-type
234
+
224
235
  if (jsonContent?.schema) {
225
236
  const responseProps = parseSchemaProperties(jsonContent.schema, schemaTypeMap);
226
237
  properties.push(...responseProps);
@@ -230,12 +241,12 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
230
241
  }
231
242
  }
232
243
  }
233
-
244
+
234
245
  // 如果沒有響應定義,返回空內容(將由調用方處理為 void)
235
246
  if (properties.length === 0) {
236
247
  return ''; // 返回空字串,讓調用方決定使用 void
237
248
  }
238
-
249
+
239
250
  return properties.join('\n');
240
251
  }
241
252
 
@@ -253,13 +264,16 @@ function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string
253
264
  const optional = isRequired ? '' : '?';
254
265
  // indentLevel=1 因為屬性已經在類型定義內(有 2 個空格縮排)
255
266
  const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1);
256
- const description = propSchema.description ? ` // ${propSchema.description}` : '';
257
267
 
258
268
  // 如果屬性名包含特殊字符(如 -),需要加上引號
259
269
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(propName);
260
270
  const quotedPropName = needsQuotes ? `"${propName}"` : propName;
261
271
 
262
- properties.push(` ${quotedPropName}${optional}: ${propType};${description}`);
272
+ // 生成 JSDoc 註解
273
+ if (propSchema.description) {
274
+ properties.push(` /** ${propSchema.description} */`);
275
+ }
276
+ properties.push(` ${quotedPropName}${optional}: ${propType};`);
263
277
  });
264
278
  }
265
279
 
@@ -320,15 +334,23 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
320
334
  // 如果有具體的屬性定義,生成內聯對象類型(多行格式)
321
335
  const entries = Object.entries(schema.properties);
322
336
 
323
- // 如果沒有屬性,返回空物件
337
+ // 如果沒有屬性但有 additionalProperties,生成 Record 類型
324
338
  if (entries.length === 0) {
325
- baseType = '{}';
339
+ if (schema.additionalProperties) {
340
+ const valueType = schema.additionalProperties === true
341
+ ? 'any'
342
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
343
+ baseType = `Record<string, ${valueType}>`;
344
+ } else {
345
+ baseType = '{}';
346
+ }
326
347
  } else {
327
348
  // 計算下一層的縮排
328
349
  const nextIndent = ' '.repeat(indentLevel + 1);
329
350
  const currentIndent = ' '.repeat(indentLevel);
330
351
 
331
- const props = entries.map(([key, propSchema]: [string, any]) => {
352
+ const props: string[] = [];
353
+ entries.forEach(([key, propSchema]: [string, any]) => {
332
354
  const required = schema.required || [];
333
355
  const optional = required.includes(key) ? '' : '?';
334
356
  const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1);
@@ -337,11 +359,21 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
337
359
  const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
338
360
  const quotedKey = needsQuotes ? `"${key}"` : key;
339
361
 
340
- return `${nextIndent}${quotedKey}${optional}: ${type};`;
341
- }).join('\n');
362
+ // 生成 JSDoc 註解
363
+ if (propSchema.description) {
364
+ props.push(`${nextIndent}/** ${propSchema.description} */`);
365
+ }
366
+ props.push(`${nextIndent}${quotedKey}${optional}: ${type};`);
367
+ });
342
368
 
343
- baseType = `{\n${props}\n${currentIndent}}`;
369
+ baseType = `{\n${props.join('\n')}\n${currentIndent}}`;
344
370
  }
371
+ } else if (schema.additionalProperties) {
372
+ // 如果沒有 properties 但有 additionalProperties
373
+ const valueType = schema.additionalProperties === true
374
+ ? 'any'
375
+ : getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
376
+ baseType = `Record<string, ${valueType}>`;
345
377
  } else {
346
378
  baseType = 'any';
347
379
  }
@@ -361,4 +393,4 @@ function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> =
361
393
  function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}): string {
362
394
  if (!param.schema) return 'any';
363
395
  return getTypeFromSchema(param.schema, schemaTypeMap);
364
- }
396
+ }
package/src/types.ts CHANGED
@@ -3,13 +3,13 @@ import type { OpenAPIV3 } from 'openapi-types';
3
3
  import ts from 'typescript';
4
4
 
5
5
  // 重新匯出服務相關類型
6
- export type {
6
+ export type {
7
7
  GroupConfig,
8
- GroupInfo
8
+ GroupInfo
9
9
  } from './services/group-service';
10
10
 
11
- export type {
12
- FileWriteResult
11
+ export type {
12
+ FileWriteResult
13
13
  } from './services/file-writer-service';
14
14
 
15
15
 
@@ -38,14 +38,14 @@ export const operationKeys = ['get', 'put', 'post', 'delete', 'options', 'head',
38
38
 
39
39
  export type GenerationOptions = Id<
40
40
  CommonOptions &
41
- Optional<OutputFileOptions, 'outputFile'> & {
42
- isDataResponse?(
43
- code: string,
44
- includeDefault: boolean,
45
- response: OpenAPIV3.ResponseObject,
46
- allResponses: OpenAPIV3.ResponsesObject
47
- ): boolean;
48
- }
41
+ Optional<OutputFileOptions, 'outputFile'> & {
42
+ isDataResponse?(
43
+ code: string,
44
+ includeDefault: boolean,
45
+ response: OpenAPIV3.ResponseObject,
46
+ allResponses: OpenAPIV3.ResponsesObject
47
+ ): boolean;
48
+ }
49
49
  >;
50
50
 
51
51
  export interface CommonOptions {
@@ -73,6 +73,15 @@ export interface CommonOptions {
73
73
  file: string;
74
74
  importReturnTypeName: string; // 用於指定別名導入,例如 IRestFulEndpointsQueryReturn
75
75
  };
76
+ /**
77
+ * Cache tag types configuration for RTK Query cache invalidation
78
+ * If provided, will import the specified type from the given file in enhanceEndpoints.ts
79
+ * Example: { file: "@/store/tagTypes", importName: "ECacheTagTypes" }
80
+ */
81
+ cacheTagTypes?: {
82
+ file: string;
83
+ importReturnTypeName: string;
84
+ };
76
85
  /**
77
86
  * defaults to "enhancedApi"
78
87
  */
@@ -189,11 +198,11 @@ export type OutputFilesConfig = {
189
198
  export type ConfigFile =
190
199
  | Id<Require<CommonOptions & OutputFileOptions, 'outputFile'>>
191
200
  | Id<
192
- Omit<CommonOptions, 'outputFile'> & {
193
- // outputFiles: { [outputFile: string]: Omit<OutputFileOptions, 'outputFile'> };
194
- outputFiles: OutputFilesConfig
195
- }
196
- >;
201
+ Omit<CommonOptions, 'outputFile'> & {
202
+ // outputFiles: { [outputFile: string]: Omit<OutputFileOptions, 'outputFile'> };
203
+ outputFiles: OutputFilesConfig
204
+ }
205
+ >;
197
206
 
198
207
  export type GenerateApiResult = {
199
208
  operationNames: string[];
@@ -226,4 +235,4 @@ export type QueryArgDefinition = {
226
235
  }
227
236
  );
228
237
 
229
- export type QueryArgDefinitions = Record<string, QueryArgDefinition>;
238
+ export type QueryArgDefinitions = Record<string, QueryArgDefinition>;