@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.
- package/README.md +8 -1
- package/lib/bin/cli.mjs +70 -57
- package/lib/bin/cli.mjs.map +1 -1
- package/lib/index.d.mts +4 -18
- package/lib/index.d.ts +4 -18
- package/lib/index.js +343 -275
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +343 -275
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/bin/utils.ts +74 -3
- package/src/generators/common-types-generator.ts +6 -2
- package/src/generators/component-schema-generator.ts +40 -7
- package/src/generators/do-not-modify-generator.ts +2 -2
- package/src/generators/rtk-enhance-endpoints-generator.ts +113 -0
- package/src/generators/{query-service-generator.ts → rtk-query-generator.ts} +45 -33
- package/src/generators/tag-types-generator.ts +30 -0
- package/src/generators/types-generator.ts +216 -112
- package/src/generators/utils-generator.ts +2 -4
- package/src/index.ts +6 -3
- package/src/services/api-code-generator.ts +62 -74
- package/src/services/endpoint-info-extractor.ts +66 -14
- package/src/services/file-writer-service.ts +27 -23
- package/src/services/openapi-parser-service.ts +10 -2
- package/src/services/unified-code-generator.ts +55 -26
- package/src/types.ts +19 -45
- package/src/generators/api-service-generator.ts +0 -112
- package/src/generators/cache-keys-generator.ts +0 -43
- package/src/generators/index-generator.ts +0 -11
- package/src/services/api-service-generator.ts +0 -24
- package/src/services/query-code-generator.ts +0 -24
|
@@ -3,81 +3,79 @@ import ts from 'typescript';
|
|
|
3
3
|
import { OpenApiParserService } from './openapi-parser-service';
|
|
4
4
|
import { EndpointInfoExtractor } from './endpoint-info-extractor';
|
|
5
5
|
import { generateTypesFile } from '../generators/types-generator';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { ApiServiceGenerator } from './api-service-generator';
|
|
6
|
+
import { generateRtkQueryFile } from '../generators/rtk-query-generator';
|
|
7
|
+
import { generateRtkEnhanceEndpointsFile } from '../generators/rtk-enhance-endpoints-generator';
|
|
9
8
|
import type { GenerationOptions, GenerateApiResult } from '../types';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
|
-
* API 程式碼生成器 - 負責生成單一群組的
|
|
13
|
-
*
|
|
11
|
+
* API 程式碼生成器 - 負責生成單一群組的 RTK Query 相關程式碼
|
|
12
|
+
*
|
|
14
13
|
* 設計理念:
|
|
15
14
|
* - 類別化管理:使用 class 封裝邏輯
|
|
16
|
-
* -
|
|
15
|
+
* - RTK Query 專用:只生成 React/RTK Query 代碼
|
|
17
16
|
* - 重用資源:接受外部已處理的 v3Doc,避免重複處理
|
|
18
17
|
*/
|
|
19
18
|
export class ApiCodeGenerator {
|
|
20
19
|
private infoExtractor: EndpointInfoExtractor;
|
|
21
|
-
private queryGenerator: QueryCodeGenerator;
|
|
22
|
-
private apiServiceGenerator: ApiServiceGenerator;
|
|
23
20
|
|
|
24
21
|
constructor(
|
|
25
22
|
private parserService: OpenApiParserService,
|
|
26
23
|
private options: GenerationOptions
|
|
27
24
|
) {
|
|
28
|
-
|
|
29
|
-
// // 初始化端點資訊提取器
|
|
25
|
+
// 初始化端點資訊提取器
|
|
30
26
|
this.infoExtractor = new EndpointInfoExtractor(options);
|
|
31
|
-
//
|
|
32
|
-
// 初始化分離的生成器
|
|
33
|
-
this.queryGenerator = new QueryCodeGenerator(options);
|
|
34
|
-
this.apiServiceGenerator = new ApiServiceGenerator(options);
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
/**
|
|
38
|
-
* 生成完整的
|
|
30
|
+
* 生成完整的 RTK Query 程式碼
|
|
39
31
|
*/
|
|
40
32
|
async generate(): Promise<GenerateApiResult> {
|
|
41
|
-
// console.log('ApiCodeGenerator: 開始生成 API 程式碼...');
|
|
42
|
-
|
|
43
33
|
// 步驟 1: 獲取操作定義
|
|
44
34
|
const operationDefinitions = this.parserService.getOperationDefinitions(this.options.filterEndpoints);
|
|
45
35
|
|
|
46
36
|
// 步驟 2: 提取端點資訊
|
|
47
37
|
const endpointInfos = this.infoExtractor.extractEndpointInfos(operationDefinitions);
|
|
48
38
|
|
|
39
|
+
// 步驟 3: 生成 RTK Query 代碼
|
|
40
|
+
return this.generateRtkQueryCode(endpointInfos);
|
|
41
|
+
}
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 生成 RTK Query 代碼
|
|
45
|
+
*/
|
|
46
|
+
private generateRtkQueryCode(endpointInfos: Array<any>): GenerateApiResult {
|
|
47
|
+
// 步驟 1: 生成 types 檔案 (React 模式也需要)
|
|
51
48
|
const typesContent = this.generateTypes(endpointInfos);
|
|
52
|
-
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
50
|
+
// 步驟 2: 生成 RTK Query 檔案
|
|
51
|
+
const rtkQueryContent = generateRtkQueryFile(endpointInfos, this.options);
|
|
52
|
+
|
|
53
|
+
// 步驟 3: 生成 enhance endpoints 檔案
|
|
54
|
+
const enhanceEndpointsContent = generateRtkEnhanceEndpointsFile(endpointInfos, this.options);
|
|
57
55
|
|
|
58
|
-
// 步驟 4:
|
|
59
|
-
const
|
|
60
|
-
.filter(info => info.isQuery) // 只收集查詢類型的端點
|
|
61
|
-
.map(info => ({
|
|
62
|
-
operationName: info.operationName,
|
|
63
|
-
queryKeyName: info.queryKeyName,
|
|
64
|
-
groupKey: this.options.groupKey || '_common'
|
|
65
|
-
}));
|
|
56
|
+
// 步驟 4: 生成 index 檔案
|
|
57
|
+
const indexContent = this.generateIndex();
|
|
66
58
|
|
|
67
|
-
// 步驟
|
|
68
|
-
const operationNames = endpointInfos.map(info => info.operationName);
|
|
59
|
+
// 步驟 6: 收集操作名稱
|
|
60
|
+
const operationNames = endpointInfos.map((info) => info.operationName);
|
|
69
61
|
|
|
70
|
-
//
|
|
62
|
+
// 步驟 7: 收集所有 tags
|
|
63
|
+
const allTags = new Set<string>();
|
|
64
|
+
endpointInfos.forEach((info) => {
|
|
65
|
+
if (info.tags && Array.isArray(info.tags)) {
|
|
66
|
+
info.tags.forEach((tag: string) => allTags.add(tag));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
71
69
|
|
|
72
70
|
return {
|
|
73
71
|
operationNames,
|
|
72
|
+
tags: Array.from(allTags),
|
|
74
73
|
files: {
|
|
75
74
|
types: typesContent,
|
|
76
|
-
|
|
77
|
-
queryService: queryServiceContent,
|
|
75
|
+
queryService: rtkQueryContent, // RTK Query 檔案
|
|
78
76
|
index: indexContent,
|
|
79
|
-
|
|
80
|
-
}
|
|
77
|
+
enhanceEndpoints: enhanceEndpointsContent, // 新增的 enhance endpoints 檔案
|
|
78
|
+
},
|
|
81
79
|
};
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -87,24 +85,27 @@ export class ApiCodeGenerator {
|
|
|
87
85
|
private generateTypes(endpointInfos: Array<any>): string {
|
|
88
86
|
const generatorOptions = {
|
|
89
87
|
...this.options,
|
|
90
|
-
apiConfiguration: this.options.apiConfiguration || {
|
|
91
|
-
file: '@/store/webapi',
|
|
92
|
-
importName: 'WebApiConfiguration'
|
|
93
|
-
}
|
|
88
|
+
apiConfiguration: this.options.apiConfiguration || {
|
|
89
|
+
file: '@/store/webapi',
|
|
90
|
+
importName: 'WebApiConfiguration',
|
|
91
|
+
},
|
|
94
92
|
};
|
|
95
93
|
|
|
96
94
|
// 從 parser service 獲取 schema interfaces
|
|
97
95
|
const apiGen = this.parserService.getApiGenerator();
|
|
98
|
-
const schemaInterfaces = apiGen.aliases.reduce<Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>>(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
const schemaInterfaces = apiGen.aliases.reduce<Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>>(
|
|
97
|
+
(curr, alias) => {
|
|
98
|
+
if (ts.isInterfaceDeclaration(alias) || ts.isTypeAliasDeclaration(alias)) {
|
|
99
|
+
const name = alias.name.text;
|
|
100
|
+
return {
|
|
101
|
+
...curr,
|
|
102
|
+
[name]: alias,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return curr;
|
|
106
|
+
},
|
|
107
|
+
{}
|
|
108
|
+
);
|
|
108
109
|
|
|
109
110
|
// 獲取操作定義以供類型生成使用
|
|
110
111
|
const operationDefinitions = this.parserService.getOperationDefinitions(this.options.filterEndpoints);
|
|
@@ -112,33 +113,20 @@ export class ApiCodeGenerator {
|
|
|
112
113
|
return generateTypesFile(endpointInfos, generatorOptions, schemaInterfaces, operationDefinitions);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
-
* 生成 API Service 檔案內容
|
|
117
|
-
*/
|
|
118
|
-
private generateApiService(endpointInfos: Array<any>): string {
|
|
119
|
-
return this.apiServiceGenerator.generateApiService(endpointInfos);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 生成 Query Service 檔案內容
|
|
124
|
-
*/
|
|
125
|
-
private generateQueryService(endpointInfos: Array<any>): string {
|
|
126
|
-
return this.queryGenerator.generateQueryService(endpointInfos);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
116
|
/**
|
|
130
117
|
* 生成 Index 檔案內容
|
|
131
118
|
*/
|
|
132
119
|
private generateIndex(): string {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
};
|
|
120
|
+
const groupKey = this.options.groupKey || '';
|
|
121
|
+
const exportName = groupKey ? `${groupKey.charAt(0).toLowerCase() + groupKey.slice(1)}Api` : 'api';
|
|
122
|
+
|
|
123
|
+
return `/* eslint-disable */
|
|
124
|
+
// [Warning] Generated automatically - do not edit manually
|
|
140
125
|
|
|
141
|
-
|
|
126
|
+
export { default as ${exportName} } from "./enhanceEndpoints";
|
|
127
|
+
export * from "./query.generated";
|
|
128
|
+
export * from "./types";
|
|
129
|
+
`;
|
|
142
130
|
}
|
|
143
131
|
|
|
144
132
|
// /**
|
|
@@ -154,4 +142,4 @@ export class ApiCodeGenerator {
|
|
|
154
142
|
// getInfoExtractor(): EndpointInfoExtractor {
|
|
155
143
|
// return this.infoExtractor;
|
|
156
144
|
// }
|
|
157
|
-
}
|
|
145
|
+
}
|
|
@@ -18,6 +18,9 @@ export interface EndpointInfo {
|
|
|
18
18
|
pathParams: any[];
|
|
19
19
|
isVoidArg: boolean;
|
|
20
20
|
summary: string;
|
|
21
|
+
contentType: string;
|
|
22
|
+
hasRequestBody: boolean;
|
|
23
|
+
tags: string[];
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -25,7 +28,10 @@ export interface EndpointInfo {
|
|
|
25
28
|
*/
|
|
26
29
|
export class EndpointInfoExtractor {
|
|
27
30
|
constructor(
|
|
28
|
-
private options: Pick<
|
|
31
|
+
private options: Pick<
|
|
32
|
+
GenerationOptions,
|
|
33
|
+
'operationNameSuffix' | 'argSuffix' | 'responseSuffix' | 'queryMatch' | 'endpointOverrides'
|
|
34
|
+
>
|
|
29
35
|
) {}
|
|
30
36
|
|
|
31
37
|
/**
|
|
@@ -44,7 +50,13 @@ export class EndpointInfoExtractor {
|
|
|
44
50
|
*/
|
|
45
51
|
private extractSingleEndpointInfo(operationDefinition: OperationDefinition): EndpointInfo {
|
|
46
52
|
const { verb, path, operation } = operationDefinition;
|
|
47
|
-
const {
|
|
53
|
+
const {
|
|
54
|
+
operationNameSuffix = '',
|
|
55
|
+
argSuffix = 'Req',
|
|
56
|
+
responseSuffix = 'Res',
|
|
57
|
+
queryMatch,
|
|
58
|
+
endpointOverrides,
|
|
59
|
+
} = this.options;
|
|
48
60
|
|
|
49
61
|
// 獲取操作名稱
|
|
50
62
|
const operationName = getOperationName({ verb, path });
|
|
@@ -56,7 +68,7 @@ export class EndpointInfoExtractor {
|
|
|
56
68
|
|
|
57
69
|
// 判斷是否為查詢類型
|
|
58
70
|
const isQuery = testIsQuery(verb, path, getOverrides(operationDefinition, endpointOverrides), queryMatch);
|
|
59
|
-
|
|
71
|
+
|
|
60
72
|
// 生成查詢鍵名稱
|
|
61
73
|
const queryKeyName = `${operationName.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
|
|
62
74
|
|
|
@@ -64,7 +76,13 @@ export class EndpointInfoExtractor {
|
|
|
64
76
|
const summary = operation.summary || `${verb.toUpperCase()} ${path}`;
|
|
65
77
|
|
|
66
78
|
// 解析參數
|
|
67
|
-
const { queryParams, pathParams, isVoidArg } = this.extractParameters(operationDefinition);
|
|
79
|
+
const { queryParams, pathParams, isVoidArg, hasRequestBody } = this.extractParameters(operationDefinition);
|
|
80
|
+
|
|
81
|
+
// 提取 content type
|
|
82
|
+
const contentType = this.extractContentType(operation);
|
|
83
|
+
|
|
84
|
+
// 提取 tags
|
|
85
|
+
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
|
68
86
|
|
|
69
87
|
return {
|
|
70
88
|
operationName: finalOperationName,
|
|
@@ -77,7 +95,10 @@ export class EndpointInfoExtractor {
|
|
|
77
95
|
queryParams,
|
|
78
96
|
pathParams,
|
|
79
97
|
isVoidArg,
|
|
80
|
-
summary
|
|
98
|
+
summary,
|
|
99
|
+
contentType,
|
|
100
|
+
hasRequestBody,
|
|
101
|
+
tags,
|
|
81
102
|
};
|
|
82
103
|
}
|
|
83
104
|
|
|
@@ -90,14 +111,19 @@ export class EndpointInfoExtractor {
|
|
|
90
111
|
|
|
91
112
|
// 解析參數
|
|
92
113
|
const operationParameters = this.resolveArray(operation.parameters);
|
|
93
|
-
const pathItemParameters = this.resolveArray(pathItem.parameters)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const allParameters = supportDeepObjects([...pathItemParameters, ...operationParameters])
|
|
97
|
-
.filter((param) => param.in !== 'header');
|
|
114
|
+
const pathItemParameters = this.resolveArray(pathItem.parameters).filter(
|
|
115
|
+
(pp) => !operationParameters.some((op) => op.name === pp.name && op.in === pp.in)
|
|
116
|
+
);
|
|
98
117
|
|
|
99
|
-
const
|
|
100
|
-
|
|
118
|
+
const allParameters = supportDeepObjects([...pathItemParameters, ...operationParameters]).filter(
|
|
119
|
+
(param) => param.in !== 'header'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const queryParams = allParameters.filter((param) => param.in === 'query');
|
|
123
|
+
const pathParams = allParameters.filter((param) => param.in === 'path');
|
|
124
|
+
|
|
125
|
+
// 檢查是否有 request body
|
|
126
|
+
const hasRequestBody = !!operation.requestBody;
|
|
101
127
|
|
|
102
128
|
// 檢查是否為 void 類型參數
|
|
103
129
|
const isVoidArg = queryParams.length === 0 && pathParams.length === 0 && !operation.requestBody;
|
|
@@ -105,7 +131,8 @@ export class EndpointInfoExtractor {
|
|
|
105
131
|
return {
|
|
106
132
|
queryParams,
|
|
107
133
|
pathParams,
|
|
108
|
-
isVoidArg
|
|
134
|
+
isVoidArg,
|
|
135
|
+
hasRequestBody,
|
|
109
136
|
};
|
|
110
137
|
}
|
|
111
138
|
|
|
@@ -116,4 +143,29 @@ export class EndpointInfoExtractor {
|
|
|
116
143
|
if (!parameters) return [];
|
|
117
144
|
return Array.isArray(parameters) ? parameters : [parameters];
|
|
118
145
|
}
|
|
119
|
-
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 提取操作的 content type
|
|
149
|
+
* @param operation - 操作對象
|
|
150
|
+
*/
|
|
151
|
+
private extractContentType(operation: any): string {
|
|
152
|
+
// 檢查 requestBody 是否存在
|
|
153
|
+
if (!operation.requestBody) {
|
|
154
|
+
return 'application/json';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 從 requestBody.content 中獲取第一個 content type
|
|
158
|
+
const content = operation.requestBody.content;
|
|
159
|
+
if (!content || typeof content !== 'object') {
|
|
160
|
+
return 'application/json';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const contentTypes = Object.keys(content);
|
|
164
|
+
if (contentTypes.length === 0) {
|
|
165
|
+
return 'application/json';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 返回第一個 content type
|
|
169
|
+
return contentTypes[0];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -23,10 +23,20 @@ export class FileWriterService {
|
|
|
23
23
|
async writeFile(filePath: string, content: string): Promise<FileWriteResult> {
|
|
24
24
|
try {
|
|
25
25
|
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
26
|
+
const fileName = path.basename(resolvedPath);
|
|
27
|
+
|
|
28
|
+
// enhanceEndpoints.ts 如果已存在則跳過寫入
|
|
29
|
+
if (fileName === 'enhanceEndpoints.ts' && fs.existsSync(resolvedPath)) {
|
|
30
|
+
return {
|
|
31
|
+
path: resolvedPath,
|
|
32
|
+
success: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
await ensureDirectoryExists(resolvedPath);
|
|
27
|
-
|
|
37
|
+
|
|
28
38
|
fs.writeFileSync(resolvedPath, content);
|
|
29
|
-
|
|
39
|
+
|
|
30
40
|
return {
|
|
31
41
|
path: resolvedPath,
|
|
32
42
|
success: true
|
|
@@ -46,27 +56,28 @@ export class FileWriterService {
|
|
|
46
56
|
*/
|
|
47
57
|
async writeFiles(files: Record<string, string>): Promise<FileWriteResult[]> {
|
|
48
58
|
const results: FileWriteResult[] = [];
|
|
49
|
-
|
|
59
|
+
|
|
50
60
|
for (const [filePath, content] of Object.entries(files)) {
|
|
51
61
|
const result = await this.writeFile(filePath, content);
|
|
52
62
|
results.push(result);
|
|
53
63
|
}
|
|
54
|
-
|
|
64
|
+
|
|
55
65
|
return results;
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
70
|
+
* 為群組寫入 RTK Query 檔案結構
|
|
60
71
|
* @param groupOutputDir - 群組輸出目錄
|
|
61
72
|
* @param files - 檔案內容
|
|
62
73
|
*/
|
|
63
74
|
async writeGroupFiles(
|
|
64
75
|
groupOutputDir: string,
|
|
65
76
|
files: {
|
|
66
|
-
types?: string;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
index?: string;
|
|
77
|
+
types?: string; // types.ts
|
|
78
|
+
queryService?: string; // query.generated.ts
|
|
79
|
+
enhanceEndpoints?: string; // enhanceEndpoints.ts
|
|
80
|
+
index?: string; // index.ts
|
|
70
81
|
}
|
|
71
82
|
): Promise<FileWriteResult[]> {
|
|
72
83
|
const filesToWrite: Record<string, string> = {};
|
|
@@ -74,15 +85,15 @@ export class FileWriterService {
|
|
|
74
85
|
if (files.types) {
|
|
75
86
|
filesToWrite[path.join(groupOutputDir, 'types.ts')] = files.types;
|
|
76
87
|
}
|
|
77
|
-
|
|
78
|
-
if (files.apiService) {
|
|
79
|
-
filesToWrite[path.join(groupOutputDir, 'api.service.ts')] = files.apiService;
|
|
80
|
-
}
|
|
81
|
-
|
|
88
|
+
|
|
82
89
|
if (files.queryService) {
|
|
83
|
-
filesToWrite[path.join(groupOutputDir, 'query.
|
|
90
|
+
filesToWrite[path.join(groupOutputDir, 'query.generated.ts')] = files.queryService;
|
|
84
91
|
}
|
|
85
|
-
|
|
92
|
+
|
|
93
|
+
if (files.enhanceEndpoints) {
|
|
94
|
+
filesToWrite[path.join(groupOutputDir, 'enhanceEndpoints.ts')] = files.enhanceEndpoints;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
if (files.index) {
|
|
87
98
|
filesToWrite[path.join(groupOutputDir, 'index.ts')] = files.index;
|
|
88
99
|
}
|
|
@@ -99,7 +110,6 @@ export class FileWriterService {
|
|
|
99
110
|
outputDir: string,
|
|
100
111
|
sharedFiles: {
|
|
101
112
|
commonTypes?: string;
|
|
102
|
-
cacheKeys?: string;
|
|
103
113
|
doNotModify?: string;
|
|
104
114
|
utils?: string;
|
|
105
115
|
}
|
|
@@ -110,9 +120,6 @@ export class FileWriterService {
|
|
|
110
120
|
filesToWrite[path.join(outputDir, 'common-types.ts')] = sharedFiles.commonTypes;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
if (sharedFiles.cacheKeys) {
|
|
114
|
-
filesToWrite[path.join(outputDir, 'cache-keys.ts')] = sharedFiles.cacheKeys;
|
|
115
|
-
}
|
|
116
123
|
|
|
117
124
|
if (sharedFiles.doNotModify) {
|
|
118
125
|
filesToWrite[path.join(outputDir, 'DO_NOT_MODIFY.md')] = sharedFiles.doNotModify;
|
|
@@ -125,9 +132,6 @@ export class FileWriterService {
|
|
|
125
132
|
return this.writeFiles(filesToWrite);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
135
|
/**
|
|
132
136
|
* 寫入共享檔案
|
|
133
137
|
* @param outputDir - 輸出目錄
|
|
@@ -15,7 +15,6 @@ export class OpenApiParserService {
|
|
|
15
15
|
) {
|
|
16
16
|
this.apiGen = new ApiGenerator(v3Doc, {
|
|
17
17
|
unionUndefined: options.unionUndefined,
|
|
18
|
-
useEnumType: options.useEnumType,
|
|
19
18
|
mergeReadWriteOnly: options.mergeReadWriteOnly,
|
|
20
19
|
});
|
|
21
20
|
}
|
|
@@ -25,8 +24,17 @@ export class OpenApiParserService {
|
|
|
25
24
|
*/
|
|
26
25
|
initialize(): void {
|
|
27
26
|
if (this.apiGen.spec.components?.schemas) {
|
|
27
|
+
// 原因:oazapfts 會優先使用 schema.title 作為類型名稱,但應該使用 schema key
|
|
28
|
+
// 這只在記憶體中修改,不影響原始 OpenAPI 文件
|
|
29
|
+
Object.keys(this.apiGen.spec.components.schemas).forEach(schemaName => {
|
|
30
|
+
const schema = this.apiGen.spec.components!.schemas![schemaName];
|
|
31
|
+
if (schema && typeof schema === 'object' && 'title' in schema) {
|
|
32
|
+
delete schema.title;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
28
36
|
this.apiGen.preprocessComponents(this.apiGen.spec.components.schemas);
|
|
29
|
-
|
|
37
|
+
|
|
30
38
|
// 手動為每個 schema 生成 type alias
|
|
31
39
|
Object.keys(this.apiGen.spec.components.schemas).forEach(schemaName => {
|
|
32
40
|
try {
|
|
@@ -4,19 +4,14 @@ import type { OpenAPIV3 } from 'openapi-types';
|
|
|
4
4
|
import { OpenApiService } from './openapi-service';
|
|
5
5
|
import { GroupService, type GroupConfig } from './group-service';
|
|
6
6
|
import { FileWriterService, type FileWriteResult } from './file-writer-service';
|
|
7
|
-
export interface EndpointCacheKey {
|
|
8
|
-
operationName: string;
|
|
9
|
-
queryKeyName: string;
|
|
10
|
-
groupKey: string;
|
|
11
|
-
}
|
|
12
7
|
import { OpenApiParserService } from './openapi-parser-service';
|
|
13
|
-
import { generateCacheKeysFile } from '../generators/cache-keys-generator';
|
|
14
8
|
import { generateCommonTypesFile } from '../generators/common-types-generator';
|
|
15
9
|
import { generateComponentSchemaFile } from '../generators/component-schema-generator';
|
|
16
10
|
import { generateDoNotModifyFile } from '../generators/do-not-modify-generator';
|
|
17
11
|
import type { GenerationOptions, CommonOptions } from '../types';
|
|
18
12
|
import { ApiCodeGenerator } from './api-code-generator';
|
|
19
13
|
import { generateUtilsFile } from '../generators/utils-generator';
|
|
14
|
+
import { generateTagTypesFile } from '../generators/tag-types-generator';
|
|
20
15
|
|
|
21
16
|
/**
|
|
22
17
|
* 統一代碼生成器選項
|
|
@@ -61,7 +56,6 @@ export class UnifiedCodeGenerator {
|
|
|
61
56
|
private openApiDoc: OpenAPIV3.Document | null = null;
|
|
62
57
|
private parserService: OpenApiParserService | null = null;
|
|
63
58
|
private schemaInterfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration> = {};
|
|
64
|
-
private allEndpointCacheKeys: EndpointCacheKey[] = [];
|
|
65
59
|
private actualSchemaFile: string = '';
|
|
66
60
|
|
|
67
61
|
// 生成內容存儲
|
|
@@ -71,20 +65,23 @@ export class UnifiedCodeGenerator {
|
|
|
71
65
|
outputPath: string;
|
|
72
66
|
content: any;
|
|
73
67
|
}>;
|
|
74
|
-
cacheKeys: string | null;
|
|
75
68
|
commonTypes: string;
|
|
76
69
|
componentSchema: string;
|
|
77
70
|
doNotModify: string;
|
|
78
71
|
utils: string;
|
|
72
|
+
tagTypes: string;
|
|
79
73
|
} = {
|
|
80
74
|
groups: [],
|
|
81
|
-
cacheKeys: null,
|
|
82
75
|
commonTypes: '',
|
|
83
76
|
componentSchema: '',
|
|
84
77
|
doNotModify: '',
|
|
85
|
-
utils: ''
|
|
78
|
+
utils: '',
|
|
79
|
+
tagTypes: ''
|
|
86
80
|
};
|
|
87
81
|
|
|
82
|
+
// 收集所有 tags
|
|
83
|
+
private allTags: Set<string> = new Set();
|
|
84
|
+
|
|
88
85
|
|
|
89
86
|
constructor(options: UnifiedGenerationOptions) {
|
|
90
87
|
this._options = options;
|
|
@@ -103,11 +100,11 @@ export class UnifiedCodeGenerator {
|
|
|
103
100
|
// await this.generateQuery();
|
|
104
101
|
|
|
105
102
|
// 生成共用
|
|
106
|
-
this.generateCacheKeysContent()
|
|
107
103
|
this.generateCommonTypesContent()
|
|
108
104
|
this.generateSchemaContent()
|
|
109
105
|
this.generateUtilsContent()
|
|
110
106
|
this.generateDoNotModifyContent()
|
|
107
|
+
this.generateTagTypesContent()
|
|
111
108
|
|
|
112
109
|
return await this.release();
|
|
113
110
|
}
|
|
@@ -186,6 +183,11 @@ export class UnifiedCodeGenerator {
|
|
|
186
183
|
outputPath: groupInfo.outputPath,
|
|
187
184
|
content: groupContent
|
|
188
185
|
});
|
|
186
|
+
|
|
187
|
+
// 收集此群組的所有 tags
|
|
188
|
+
if (groupContent.tags && Array.isArray(groupContent.tags)) {
|
|
189
|
+
groupContent.tags.forEach((tag: string) => this.allTags.add(tag));
|
|
190
|
+
}
|
|
189
191
|
}
|
|
190
192
|
// 如果沒有任何 endpoint,則跳過此群組,不創建資料夾
|
|
191
193
|
} catch (error) {
|
|
@@ -197,12 +199,6 @@ export class UnifiedCodeGenerator {
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
|
|
200
|
-
/**
|
|
201
|
-
* 生成 cache keys
|
|
202
|
-
*/
|
|
203
|
-
private async generateCacheKeysContent(): Promise<void> {
|
|
204
|
-
this.generatedContent.cacheKeys = generateCacheKeysFile(this.allEndpointCacheKeys);
|
|
205
|
-
}
|
|
206
202
|
|
|
207
203
|
/**
|
|
208
204
|
* 生成 common types
|
|
@@ -233,6 +229,14 @@ export class UnifiedCodeGenerator {
|
|
|
233
229
|
this.generatedContent.utils = generateUtilsFile();
|
|
234
230
|
}
|
|
235
231
|
|
|
232
|
+
/**
|
|
233
|
+
* 生成 Tag Types
|
|
234
|
+
*/
|
|
235
|
+
private async generateTagTypesContent(): Promise<void> {
|
|
236
|
+
const tagsArray = Array.from(this.allTags);
|
|
237
|
+
this.generatedContent.tagTypes = generateTagTypesFile(tagsArray);
|
|
238
|
+
}
|
|
239
|
+
|
|
236
240
|
|
|
237
241
|
|
|
238
242
|
|
|
@@ -252,15 +256,17 @@ export class UnifiedCodeGenerator {
|
|
|
252
256
|
try {
|
|
253
257
|
if (group.content?.files) {
|
|
254
258
|
const groupOutputDir = path.dirname(group.outputPath);
|
|
259
|
+
// RTK Query 模式 (唯一支援模式)
|
|
255
260
|
const groupResults = await this.fileWriterService.writeGroupFiles(
|
|
256
261
|
groupOutputDir,
|
|
257
262
|
{
|
|
258
263
|
types: group.content.files.types,
|
|
259
|
-
apiService: group.content.files.apiService,
|
|
260
264
|
queryService: group.content.files.queryService,
|
|
265
|
+
enhanceEndpoints: group.content.files.enhanceEndpoints,
|
|
261
266
|
index: group.content.files.index
|
|
262
267
|
}
|
|
263
268
|
);
|
|
269
|
+
|
|
264
270
|
results.push(...groupResults);
|
|
265
271
|
generatedGroups.push(group.groupKey);
|
|
266
272
|
}
|
|
@@ -275,11 +281,10 @@ export class UnifiedCodeGenerator {
|
|
|
275
281
|
'./generated';
|
|
276
282
|
|
|
277
283
|
// 寫入共用檔案 (包含 DO_NOT_MODIFY.md)
|
|
278
|
-
if (this.generatedContent.
|
|
284
|
+
if (this.generatedContent.commonTypes || this.generatedContent.doNotModify || this.generatedContent.utils) {
|
|
279
285
|
const sharedResults = await this.fileWriterService.writeSharedFiles(
|
|
280
286
|
outputDir,
|
|
281
|
-
{
|
|
282
|
-
cacheKeys: this.generatedContent.cacheKeys || undefined,
|
|
287
|
+
{
|
|
283
288
|
commonTypes: this.generatedContent.commonTypes || undefined,
|
|
284
289
|
doNotModify: this.generatedContent.doNotModify || undefined,
|
|
285
290
|
utils: this.generatedContent.utils || undefined
|
|
@@ -288,6 +293,15 @@ export class UnifiedCodeGenerator {
|
|
|
288
293
|
results.push(...sharedResults);
|
|
289
294
|
}
|
|
290
295
|
|
|
296
|
+
// 寫入 tagTypes.ts
|
|
297
|
+
if (this.generatedContent.tagTypes) {
|
|
298
|
+
const tagTypesResult = await this.fileWriterService.writeFile(
|
|
299
|
+
path.join(outputDir, 'tagTypes.ts'),
|
|
300
|
+
this.generatedContent.tagTypes
|
|
301
|
+
);
|
|
302
|
+
results.push(tagTypesResult);
|
|
303
|
+
}
|
|
304
|
+
|
|
291
305
|
// 寫入 component schema
|
|
292
306
|
if (this.generatedContent.componentSchema) {
|
|
293
307
|
const schemaResults = await this.fileWriterService.writeSchemaFile(
|
|
@@ -297,6 +311,14 @@ export class UnifiedCodeGenerator {
|
|
|
297
311
|
results.push(...schemaResults);
|
|
298
312
|
}
|
|
299
313
|
|
|
314
|
+
// 生成主 index.ts 檔案
|
|
315
|
+
const mainIndexContent = this.generateMainIndex(generatedGroups);
|
|
316
|
+
const mainIndexResult = await this.fileWriterService.writeFile(
|
|
317
|
+
path.join(outputDir, 'index.ts'),
|
|
318
|
+
mainIndexContent
|
|
319
|
+
);
|
|
320
|
+
results.push(mainIndexResult);
|
|
321
|
+
|
|
300
322
|
} catch (error) {
|
|
301
323
|
errors.push(error as Error);
|
|
302
324
|
}
|
|
@@ -340,14 +362,21 @@ export class UnifiedCodeGenerator {
|
|
|
340
362
|
const apiGenerator = new ApiCodeGenerator(this.parserService, groupOptions);
|
|
341
363
|
const result = await apiGenerator.generate();
|
|
342
364
|
|
|
343
|
-
// 提取並儲存快取鍵
|
|
344
|
-
if (result.files && 'allEndpointCacheKeys' in result.files) {
|
|
345
|
-
const cacheKeys = result.files.allEndpointCacheKeys as EndpointCacheKey[];
|
|
346
|
-
this.allEndpointCacheKeys.push(...cacheKeys);
|
|
347
|
-
}
|
|
348
365
|
|
|
349
366
|
return result;
|
|
350
367
|
}
|
|
351
368
|
|
|
369
|
+
/**
|
|
370
|
+
* 生成主 index.ts 檔案
|
|
371
|
+
*/
|
|
372
|
+
private generateMainIndex(generatedGroups: string[]): string {
|
|
373
|
+
const exports = generatedGroups.map(groupKey => `export * from "./${groupKey}";`).join('\n');
|
|
374
|
+
|
|
375
|
+
return `/* eslint-disable */
|
|
376
|
+
// [Warning] Generated automatically - do not edit manually
|
|
377
|
+
|
|
378
|
+
${exports}
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
352
381
|
|
|
353
382
|
}
|