@bitstack/ng-query-codegen-openapi 0.0.30
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 +77 -0
- package/lib/bin/cli.mjs +186 -0
- package/lib/bin/cli.mjs.map +1 -0
- package/lib/index.d.mts +191 -0
- package/lib/index.d.ts +191 -0
- package/lib/index.js +1392 -0
- package/lib/index.js.map +1 -0
- package/lib/index.mjs +1376 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +91 -0
- package/src/bin/cli.ts +77 -0
- package/src/bin/utils.ts +85 -0
- package/src/generators/api-service-generator.ts +112 -0
- package/src/generators/cache-keys-generator.ts +43 -0
- package/src/generators/common-types-generator.ts +33 -0
- package/src/generators/component-schema-generator.ts +25 -0
- package/src/generators/do-not-modify-generator.ts +27 -0
- package/src/generators/index-generator.ts +11 -0
- package/src/generators/query-service-generator.ts +108 -0
- package/src/generators/types-generator.ts +285 -0
- package/src/generators/utils-generator.ts +33 -0
- package/src/index.ts +21 -0
- package/src/services/api-code-generator.ts +157 -0
- package/src/services/api-service-generator.ts +24 -0
- package/src/services/endpoint-info-extractor.ts +119 -0
- package/src/services/file-writer-service.ts +148 -0
- package/src/services/group-service.ts +84 -0
- package/src/services/openapi-parser-service.ts +72 -0
- package/src/services/openapi-service.ts +61 -0
- package/src/services/query-code-generator.ts +24 -0
- package/src/services/unified-code-generator.ts +353 -0
- package/src/types.ts +248 -0
- package/src/utils/capitalize.ts +3 -0
- package/src/utils/directory.ts +75 -0
- package/src/utils/downloadSchema.ts +33 -0
- package/src/utils/factory.ts +29 -0
- package/src/utils/getOperationDefinitions.ts +20 -0
- package/src/utils/getV3Doc.ts +24 -0
- package/src/utils/http.ts +86 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/isQuery.ts +16 -0
- package/src/utils/isValidUrl.ts +9 -0
- package/src/utils/messages.ts +7 -0
- package/src/utils/prettier.ts +51 -0
- package/src/utils/removeUndefined.ts +3 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { OpenApiParserService } from './openapi-parser-service';
|
|
4
|
+
import { EndpointInfoExtractor } from './endpoint-info-extractor';
|
|
5
|
+
import { generateTypesFile } from '../generators/types-generator';
|
|
6
|
+
import { generateIndexFile } from '../generators/index-generator';
|
|
7
|
+
import { QueryCodeGenerator } from './query-code-generator';
|
|
8
|
+
import { ApiServiceGenerator } from './api-service-generator';
|
|
9
|
+
import type { GenerationOptions, GenerateApiResult } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* API 程式碼生成器 - 負責生成單一群組的 API 相關程式碼
|
|
13
|
+
*
|
|
14
|
+
* 設計理念:
|
|
15
|
+
* - 類別化管理:使用 class 封裝邏輯
|
|
16
|
+
* - 分離關注點:將 Query 和 API 服務分開生成
|
|
17
|
+
* - 重用資源:接受外部已處理的 v3Doc,避免重複處理
|
|
18
|
+
*/
|
|
19
|
+
export class ApiCodeGenerator {
|
|
20
|
+
private infoExtractor: EndpointInfoExtractor;
|
|
21
|
+
private queryGenerator: QueryCodeGenerator;
|
|
22
|
+
private apiServiceGenerator: ApiServiceGenerator;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private parserService: OpenApiParserService,
|
|
26
|
+
private options: GenerationOptions
|
|
27
|
+
) {
|
|
28
|
+
|
|
29
|
+
// // 初始化端點資訊提取器
|
|
30
|
+
this.infoExtractor = new EndpointInfoExtractor(options);
|
|
31
|
+
//
|
|
32
|
+
// 初始化分離的生成器
|
|
33
|
+
this.queryGenerator = new QueryCodeGenerator(options);
|
|
34
|
+
this.apiServiceGenerator = new ApiServiceGenerator(options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 生成完整的 API 程式碼
|
|
39
|
+
*/
|
|
40
|
+
async generate(): Promise<GenerateApiResult> {
|
|
41
|
+
// console.log('ApiCodeGenerator: 開始生成 API 程式碼...');
|
|
42
|
+
|
|
43
|
+
// 步驟 1: 獲取操作定義
|
|
44
|
+
const operationDefinitions = this.parserService.getOperationDefinitions(this.options.filterEndpoints);
|
|
45
|
+
|
|
46
|
+
// 步驟 2: 提取端點資訊
|
|
47
|
+
const endpointInfos = this.infoExtractor.extractEndpointInfos(operationDefinitions);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// 步驟 3: 生成各種內容
|
|
51
|
+
const typesContent = this.generateTypes(endpointInfos);
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const apiServiceContent = this.generateApiService(endpointInfos);
|
|
55
|
+
const queryServiceContent = this.generateQueryService(endpointInfos);
|
|
56
|
+
const indexContent = this.generateIndex();
|
|
57
|
+
|
|
58
|
+
// 步驟 4: 收集端點快取鍵資訊(只收集 Query 類型的端點)
|
|
59
|
+
const allEndpointCacheKeys = endpointInfos
|
|
60
|
+
.filter(info => info.isQuery) // 只收集查詢類型的端點
|
|
61
|
+
.map(info => ({
|
|
62
|
+
operationName: info.operationName,
|
|
63
|
+
queryKeyName: info.queryKeyName,
|
|
64
|
+
groupKey: this.options.groupKey || '_common'
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// 步驟 5: 收集操作名稱
|
|
68
|
+
const operationNames = endpointInfos.map(info => info.operationName);
|
|
69
|
+
|
|
70
|
+
// console.log('ApiCodeGenerator: API 程式碼生成完成');
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
operationNames,
|
|
74
|
+
files: {
|
|
75
|
+
types: typesContent,
|
|
76
|
+
apiService: apiServiceContent,
|
|
77
|
+
queryService: queryServiceContent,
|
|
78
|
+
index: indexContent,
|
|
79
|
+
allEndpointCacheKeys: allEndpointCacheKeys
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 生成 Types 檔案內容
|
|
86
|
+
*/
|
|
87
|
+
private generateTypes(endpointInfos: Array<any>): string {
|
|
88
|
+
const generatorOptions = {
|
|
89
|
+
...this.options,
|
|
90
|
+
apiConfiguration: this.options.apiConfiguration || {
|
|
91
|
+
file: '@/store/webapi',
|
|
92
|
+
importName: 'WebApiConfiguration'
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// 從 parser service 獲取 schema interfaces
|
|
97
|
+
const apiGen = this.parserService.getApiGenerator();
|
|
98
|
+
const schemaInterfaces = apiGen.aliases.reduce<Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>>((curr, alias) => {
|
|
99
|
+
if (ts.isInterfaceDeclaration(alias) || ts.isTypeAliasDeclaration(alias)) {
|
|
100
|
+
const name = alias.name.text;
|
|
101
|
+
return {
|
|
102
|
+
...curr,
|
|
103
|
+
[name]: alias,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return curr;
|
|
107
|
+
}, {});
|
|
108
|
+
|
|
109
|
+
// 獲取操作定義以供類型生成使用
|
|
110
|
+
const operationDefinitions = this.parserService.getOperationDefinitions(this.options.filterEndpoints);
|
|
111
|
+
|
|
112
|
+
return generateTypesFile(endpointInfos, generatorOptions, schemaInterfaces, operationDefinitions);
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
/**
|
|
130
|
+
* 生成 Index 檔案內容
|
|
131
|
+
*/
|
|
132
|
+
private generateIndex(): string {
|
|
133
|
+
const generatorOptions = {
|
|
134
|
+
...this.options,
|
|
135
|
+
apiConfiguration: this.options.apiConfiguration || {
|
|
136
|
+
file: '@/store/webapi',
|
|
137
|
+
importName: 'WebApiConfiguration'
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return generateIndexFile(this.options.groupKey || '', generatorOptions);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// /**
|
|
145
|
+
// * 獲取已解析的 parser service(供外部使用)
|
|
146
|
+
// */
|
|
147
|
+
// getParserService(): OpenApiParserService {
|
|
148
|
+
// return this.parserService;
|
|
149
|
+
// }
|
|
150
|
+
//
|
|
151
|
+
// /**
|
|
152
|
+
// * 獲取端點資訊提取器(供外部使用)
|
|
153
|
+
// */
|
|
154
|
+
// getInfoExtractor(): EndpointInfoExtractor {
|
|
155
|
+
// return this.infoExtractor;
|
|
156
|
+
// }
|
|
157
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GenerationOptions } from '../types';
|
|
2
|
+
import { generateApiServiceFile } from '../generators/api-service-generator';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API 服務程式碼生成器 - 專門負責生成 API Service 相關程式碼
|
|
6
|
+
*/
|
|
7
|
+
export class ApiServiceGenerator {
|
|
8
|
+
constructor(private options: GenerationOptions) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 生成 API Service 檔案內容
|
|
12
|
+
*/
|
|
13
|
+
generateApiService(endpointInfos: Array<any>): string {
|
|
14
|
+
const generatorOptions = {
|
|
15
|
+
...this.options,
|
|
16
|
+
apiConfiguration: this.options.apiConfiguration || {
|
|
17
|
+
file: '@/store/webapi',
|
|
18
|
+
importName: 'WebApiConfiguration'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return generateApiServiceFile(endpointInfos, generatorOptions);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { capitalize, isQuery as testIsQuery } from '../utils';
|
|
2
|
+
import { getOperationName, getOverrides } from '../utils/http';
|
|
3
|
+
import type { OperationDefinition, GenerationOptions } from '../types';
|
|
4
|
+
import { supportDeepObjects } from 'oazapfts/generate';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 端點資訊介面
|
|
8
|
+
*/
|
|
9
|
+
export interface EndpointInfo {
|
|
10
|
+
operationName: string;
|
|
11
|
+
argTypeName: string;
|
|
12
|
+
responseTypeName: string;
|
|
13
|
+
isQuery: boolean;
|
|
14
|
+
verb: string;
|
|
15
|
+
path: string;
|
|
16
|
+
queryKeyName: string;
|
|
17
|
+
queryParams: any[];
|
|
18
|
+
pathParams: any[];
|
|
19
|
+
isVoidArg: boolean;
|
|
20
|
+
summary: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 端點資訊提取器 - 專門負責從操作定義中提取端點資訊
|
|
25
|
+
*/
|
|
26
|
+
export class EndpointInfoExtractor {
|
|
27
|
+
constructor(
|
|
28
|
+
private options: Pick<GenerationOptions, 'operationNameSuffix' | 'argSuffix' | 'responseSuffix' | 'queryMatch' | 'endpointOverrides'>
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 從操作定義列表提取端點資訊
|
|
33
|
+
* @param operationDefinitions - 操作定義列表
|
|
34
|
+
*/
|
|
35
|
+
extractEndpointInfos(operationDefinitions: OperationDefinition[]): EndpointInfo[] {
|
|
36
|
+
return operationDefinitions.map((operationDefinition) => {
|
|
37
|
+
return this.extractSingleEndpointInfo(operationDefinition);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 從單一操作定義提取端點資訊
|
|
43
|
+
* @param operationDefinition - 操作定義
|
|
44
|
+
*/
|
|
45
|
+
private extractSingleEndpointInfo(operationDefinition: OperationDefinition): EndpointInfo {
|
|
46
|
+
const { verb, path, operation } = operationDefinition;
|
|
47
|
+
const { operationNameSuffix = '', argSuffix = 'Req', responseSuffix = 'Res', queryMatch, endpointOverrides } = this.options;
|
|
48
|
+
|
|
49
|
+
// 獲取操作名稱
|
|
50
|
+
const operationName = getOperationName({ verb, path });
|
|
51
|
+
const finalOperationName = operationNameSuffix ? capitalize(operationName + operationNameSuffix) : operationName;
|
|
52
|
+
|
|
53
|
+
// 生成類型名稱
|
|
54
|
+
const argTypeName = capitalize(operationName + operationNameSuffix + argSuffix);
|
|
55
|
+
const responseTypeName = capitalize(operationName + operationNameSuffix + responseSuffix);
|
|
56
|
+
|
|
57
|
+
// 判斷是否為查詢類型
|
|
58
|
+
const isQuery = testIsQuery(verb, path, getOverrides(operationDefinition, endpointOverrides), queryMatch);
|
|
59
|
+
|
|
60
|
+
// 生成查詢鍵名稱
|
|
61
|
+
const queryKeyName = `${operationName.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
|
|
62
|
+
|
|
63
|
+
// 提取 OpenAPI summary
|
|
64
|
+
const summary = operation.summary || `${verb.toUpperCase()} ${path}`;
|
|
65
|
+
|
|
66
|
+
// 解析參數
|
|
67
|
+
const { queryParams, pathParams, isVoidArg } = this.extractParameters(operationDefinition);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
operationName: finalOperationName,
|
|
71
|
+
argTypeName,
|
|
72
|
+
responseTypeName,
|
|
73
|
+
isQuery,
|
|
74
|
+
verb: verb.toUpperCase(),
|
|
75
|
+
path,
|
|
76
|
+
queryKeyName,
|
|
77
|
+
queryParams,
|
|
78
|
+
pathParams,
|
|
79
|
+
isVoidArg,
|
|
80
|
+
summary
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 提取操作的參數資訊
|
|
86
|
+
* @param operationDefinition - 操作定義
|
|
87
|
+
*/
|
|
88
|
+
private extractParameters(operationDefinition: OperationDefinition) {
|
|
89
|
+
const { operation, pathItem } = operationDefinition;
|
|
90
|
+
|
|
91
|
+
// 解析參數
|
|
92
|
+
const operationParameters = this.resolveArray(operation.parameters);
|
|
93
|
+
const pathItemParameters = this.resolveArray(pathItem.parameters)
|
|
94
|
+
.filter((pp) => !operationParameters.some((op) => op.name === pp.name && op.in === pp.in));
|
|
95
|
+
|
|
96
|
+
const allParameters = supportDeepObjects([...pathItemParameters, ...operationParameters])
|
|
97
|
+
.filter((param) => param.in !== 'header');
|
|
98
|
+
|
|
99
|
+
const queryParams = allParameters.filter(param => param.in === 'query');
|
|
100
|
+
const pathParams = allParameters.filter(param => param.in === 'path');
|
|
101
|
+
|
|
102
|
+
// 檢查是否為 void 類型參數
|
|
103
|
+
const isVoidArg = queryParams.length === 0 && pathParams.length === 0 && !operation.requestBody;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
queryParams,
|
|
107
|
+
pathParams,
|
|
108
|
+
isVoidArg
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 解析參數陣列 (模擬 apiGen.resolveArray)
|
|
114
|
+
*/
|
|
115
|
+
private resolveArray(parameters: any): any[] {
|
|
116
|
+
if (!parameters) return [];
|
|
117
|
+
return Array.isArray(parameters) ? parameters : [parameters];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDirectoryExists } from '../utils/directory';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 檔案寫入結果
|
|
7
|
+
*/
|
|
8
|
+
export interface FileWriteResult {
|
|
9
|
+
path: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
error?: Error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 檔案寫入服務 - 負責將產生的內容寫入檔案
|
|
16
|
+
*/
|
|
17
|
+
export class FileWriterService {
|
|
18
|
+
/**
|
|
19
|
+
* 寫入單一檔案
|
|
20
|
+
* @param filePath - 檔案路徑
|
|
21
|
+
* @param content - 檔案內容
|
|
22
|
+
*/
|
|
23
|
+
async writeFile(filePath: string, content: string): Promise<FileWriteResult> {
|
|
24
|
+
try {
|
|
25
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
26
|
+
await ensureDirectoryExists(resolvedPath);
|
|
27
|
+
|
|
28
|
+
fs.writeFileSync(resolvedPath, content);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
path: resolvedPath,
|
|
32
|
+
success: true
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
path: filePath,
|
|
37
|
+
success: false,
|
|
38
|
+
error: error as Error
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 批次寫入多個檔案
|
|
45
|
+
* @param files - 檔案路徑與內容的對應表
|
|
46
|
+
*/
|
|
47
|
+
async writeFiles(files: Record<string, string>): Promise<FileWriteResult[]> {
|
|
48
|
+
const results: FileWriteResult[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
51
|
+
const result = await this.writeFile(filePath, content);
|
|
52
|
+
results.push(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 為群組寫入標準檔案結構
|
|
60
|
+
* @param groupOutputDir - 群組輸出目錄
|
|
61
|
+
* @param files - 檔案內容
|
|
62
|
+
*/
|
|
63
|
+
async writeGroupFiles(
|
|
64
|
+
groupOutputDir: string,
|
|
65
|
+
files: {
|
|
66
|
+
types?: string;
|
|
67
|
+
apiService?: string;
|
|
68
|
+
queryService?: string;
|
|
69
|
+
index?: string;
|
|
70
|
+
}
|
|
71
|
+
): Promise<FileWriteResult[]> {
|
|
72
|
+
const filesToWrite: Record<string, string> = {};
|
|
73
|
+
|
|
74
|
+
if (files.types) {
|
|
75
|
+
filesToWrite[path.join(groupOutputDir, 'types.ts')] = files.types;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (files.apiService) {
|
|
79
|
+
filesToWrite[path.join(groupOutputDir, 'api.service.ts')] = files.apiService;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (files.queryService) {
|
|
83
|
+
filesToWrite[path.join(groupOutputDir, 'query.service.ts')] = files.queryService;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (files.index) {
|
|
87
|
+
filesToWrite[path.join(groupOutputDir, 'index.ts')] = files.index;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.writeFiles(filesToWrite);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 寫入共享檔案
|
|
95
|
+
* @param outputDir - 輸出目錄
|
|
96
|
+
* @param sharedFiles - 共享檔案內容
|
|
97
|
+
*/
|
|
98
|
+
async writeSharedFiles(
|
|
99
|
+
outputDir: string,
|
|
100
|
+
sharedFiles: {
|
|
101
|
+
commonTypes?: string;
|
|
102
|
+
cacheKeys?: string;
|
|
103
|
+
doNotModify?: string;
|
|
104
|
+
utils?: string;
|
|
105
|
+
}
|
|
106
|
+
): Promise<FileWriteResult[]> {
|
|
107
|
+
const filesToWrite: Record<string, string> = {};
|
|
108
|
+
|
|
109
|
+
if (sharedFiles.commonTypes) {
|
|
110
|
+
filesToWrite[path.join(outputDir, 'common-types.ts')] = sharedFiles.commonTypes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sharedFiles.cacheKeys) {
|
|
114
|
+
filesToWrite[path.join(outputDir, 'cache-keys.ts')] = sharedFiles.cacheKeys;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (sharedFiles.doNotModify) {
|
|
118
|
+
filesToWrite[path.join(outputDir, 'DO_NOT_MODIFY.md')] = sharedFiles.doNotModify;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (sharedFiles.utils) {
|
|
122
|
+
filesToWrite[path.join(outputDir, 'utils.ts')] = sharedFiles.utils;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.writeFiles(filesToWrite);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 寫入共享檔案
|
|
133
|
+
* @param outputDir - 輸出目錄
|
|
134
|
+
* @param schema
|
|
135
|
+
*/
|
|
136
|
+
async writeSchemaFile(
|
|
137
|
+
outputDir: string,
|
|
138
|
+
schema: string
|
|
139
|
+
): Promise<FileWriteResult[]> {
|
|
140
|
+
const filesToWrite: Record<string, string> = {};
|
|
141
|
+
|
|
142
|
+
filesToWrite[path.join(outputDir, 'schema.ts')] = schema;
|
|
143
|
+
|
|
144
|
+
return this.writeFiles(filesToWrite);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import camelCase from 'lodash.camelcase';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 群組配置介面
|
|
5
|
+
*/
|
|
6
|
+
export interface GroupConfig {
|
|
7
|
+
outputDir: string;
|
|
8
|
+
groupKeyMatch: (path: string) => string | null;
|
|
9
|
+
filterEndpoint?: (operationName: string, path: string, groupKey: string) => boolean;
|
|
10
|
+
queryMatch?: (operationName: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 群組信息介面
|
|
15
|
+
*/
|
|
16
|
+
export interface GroupInfo {
|
|
17
|
+
groupKey: string;
|
|
18
|
+
paths: string[];
|
|
19
|
+
outputPath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 群組服務 - 負責處理 API 路徑分組
|
|
24
|
+
*/
|
|
25
|
+
export class GroupService {
|
|
26
|
+
/**
|
|
27
|
+
* 根據配置對路徑進行分組
|
|
28
|
+
* @param paths - API 路徑陣列
|
|
29
|
+
* @param config - 群組配置
|
|
30
|
+
*/
|
|
31
|
+
groupPaths(paths: string[], config: GroupConfig): Record<string, GroupInfo> {
|
|
32
|
+
const { groupKeyMatch, outputDir } = config;
|
|
33
|
+
|
|
34
|
+
const groupedPaths = paths.reduce((acc, path) => {
|
|
35
|
+
const rawGroupKey = groupKeyMatch(path);
|
|
36
|
+
const groupKey = rawGroupKey ? camelCase(rawGroupKey) : '_common';
|
|
37
|
+
|
|
38
|
+
if (!acc[groupKey]) {
|
|
39
|
+
acc[groupKey] = [];
|
|
40
|
+
}
|
|
41
|
+
acc[groupKey].push(path);
|
|
42
|
+
return acc;
|
|
43
|
+
}, {} as Record<string, string[]>);
|
|
44
|
+
|
|
45
|
+
// 轉換為 GroupInfo 格式
|
|
46
|
+
const result: Record<string, GroupInfo> = {};
|
|
47
|
+
for (const [groupKey, paths] of Object.entries(groupedPaths)) {
|
|
48
|
+
result[groupKey] = {
|
|
49
|
+
groupKey,
|
|
50
|
+
paths,
|
|
51
|
+
outputPath: `${outputDir}/${groupKey}/query.service.ts`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 為特定群組建立篩選函數
|
|
60
|
+
* @param groupKey - 群組鍵
|
|
61
|
+
* @param config - 群組配置
|
|
62
|
+
*/
|
|
63
|
+
createGroupFilter(
|
|
64
|
+
groupKey: string,
|
|
65
|
+
config: GroupConfig
|
|
66
|
+
): (operationName: string, operationDefinition: any) => boolean {
|
|
67
|
+
return (operationName: string, operationDefinition: any) => {
|
|
68
|
+
const path = operationDefinition.path;
|
|
69
|
+
|
|
70
|
+
// 檢查路徑是否匹配當前分組
|
|
71
|
+
const pathGroupKey = camelCase(config.groupKeyMatch(path) || '');
|
|
72
|
+
if (pathGroupKey !== groupKey && (pathGroupKey || '_common') !== groupKey) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 使用 filterEndpoint 進行額外篩選
|
|
77
|
+
if (config.filterEndpoint) {
|
|
78
|
+
return config.filterEndpoint(operationName, path, groupKey);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ApiGenerator from 'oazapfts/generate';
|
|
2
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import type { GenerationOptions } from '../types';
|
|
4
|
+
import { getOperationDefinitions } from '../utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenAPI 解析器服務 - 負責解析 OpenAPI 文檔並提取相關數據
|
|
8
|
+
*/
|
|
9
|
+
export class OpenApiParserService {
|
|
10
|
+
private apiGen: ApiGenerator;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private v3Doc: OpenAPIV3.Document,
|
|
14
|
+
options: Partial<GenerationOptions>
|
|
15
|
+
) {
|
|
16
|
+
this.apiGen = new ApiGenerator(v3Doc, {
|
|
17
|
+
unionUndefined: options.unionUndefined,
|
|
18
|
+
useEnumType: options.useEnumType,
|
|
19
|
+
mergeReadWriteOnly: options.mergeReadWriteOnly,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 初始化 - 預處理組件
|
|
25
|
+
*/
|
|
26
|
+
initialize(): void {
|
|
27
|
+
if (this.apiGen.spec.components?.schemas) {
|
|
28
|
+
this.apiGen.preprocessComponents(this.apiGen.spec.components.schemas);
|
|
29
|
+
|
|
30
|
+
// 手動為每個 schema 生成 type alias
|
|
31
|
+
Object.keys(this.apiGen.spec.components.schemas).forEach(schemaName => {
|
|
32
|
+
try {
|
|
33
|
+
this.apiGen.getRefAlias({ $ref: `#/components/schemas/${schemaName}` });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// 忽略無法生成的 schema
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 獲取操作定義列表
|
|
43
|
+
* @param filterEndpoints - 端點過濾函數
|
|
44
|
+
*/
|
|
45
|
+
getOperationDefinitions(filterEndpoints?: any) {
|
|
46
|
+
const { operationMatches } = require('../utils/http');
|
|
47
|
+
return getOperationDefinitions(this.v3Doc).filter(operationMatches(filterEndpoints));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 獲取 API 生成器實例
|
|
52
|
+
*/
|
|
53
|
+
getApiGenerator(): ApiGenerator {
|
|
54
|
+
return this.apiGen;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 獲取 OpenAPI 文檔
|
|
59
|
+
*/
|
|
60
|
+
getDocument(): OpenAPIV3.Document {
|
|
61
|
+
return this.v3Doc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 獲取所有 schema 類型名稱
|
|
66
|
+
*/
|
|
67
|
+
getSchemaTypeNames(): Set<string> {
|
|
68
|
+
const schemeTypeNames = new Set<string>();
|
|
69
|
+
// 這裡可以根據需要添加 schema 類型名稱的提取邏輯
|
|
70
|
+
return schemeTypeNames;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { getV3Doc, downloadSchemaFile } from '../utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpenAPI 服務 - 負責獲取和處理 OpenAPI 文檔
|
|
6
|
+
*
|
|
7
|
+
* 單一職責:
|
|
8
|
+
* - 從本地文件或遠程URL獲取OpenAPI文檔
|
|
9
|
+
* - 管理文檔快取
|
|
10
|
+
* - 提供文檔基本操作方法
|
|
11
|
+
*
|
|
12
|
+
* 設計原則:
|
|
13
|
+
* - 非收集模式:每次調用都是獨立的操作
|
|
14
|
+
* - 無副作用:不修改傳入的參數
|
|
15
|
+
* - 可測試性:所有方法都有明確的輸入輸出
|
|
16
|
+
*/
|
|
17
|
+
export class OpenApiService {
|
|
18
|
+
private docCache: Record<string, OpenAPIV3.Document> = {};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 獲取 OpenAPI 文檔
|
|
22
|
+
* @param schemaLocation - Schema 位置 (URL 或本地路徑)
|
|
23
|
+
* @param httpResolverOptions - HTTP 解析選項
|
|
24
|
+
*/
|
|
25
|
+
async getDocument(
|
|
26
|
+
schemaLocation: string,
|
|
27
|
+
httpResolverOptions?: any
|
|
28
|
+
): Promise<OpenAPIV3.Document> {
|
|
29
|
+
if (this.docCache[schemaLocation]) {
|
|
30
|
+
return this.docCache[schemaLocation];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const doc = await getV3Doc(schemaLocation, httpResolverOptions);
|
|
34
|
+
this.docCache[schemaLocation] = doc;
|
|
35
|
+
return doc;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 下載遠程 Schema 文件
|
|
40
|
+
* @param remoteUrl - 遠程 URL
|
|
41
|
+
* @param localPath - 本地儲存路徑
|
|
42
|
+
*/
|
|
43
|
+
async downloadSchema(remoteUrl: string, localPath: string): Promise<string> {
|
|
44
|
+
return downloadSchemaFile(remoteUrl, localPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 獲取所有 API 路徑
|
|
49
|
+
* @param doc - OpenAPI 文檔
|
|
50
|
+
*/
|
|
51
|
+
getPaths(doc: OpenAPIV3.Document): string[] {
|
|
52
|
+
return Object.keys(doc.paths || {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 清除快取
|
|
57
|
+
*/
|
|
58
|
+
clearCache(): void {
|
|
59
|
+
this.docCache = {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GenerationOptions } from '../types';
|
|
2
|
+
import { generateQueryServiceFile } from '../generators/query-service-generator';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Query 服務程式碼生成器 - 專門負責生成 Query Service 相關程式碼
|
|
6
|
+
*/
|
|
7
|
+
export class QueryCodeGenerator {
|
|
8
|
+
constructor(private options: GenerationOptions) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 生成 Query Service 檔案內容
|
|
12
|
+
*/
|
|
13
|
+
generateQueryService(endpointInfos: Array<any>): string {
|
|
14
|
+
const generatorOptions = {
|
|
15
|
+
...this.options,
|
|
16
|
+
apiConfiguration: this.options.apiConfiguration || {
|
|
17
|
+
file: '@/store/webapi',
|
|
18
|
+
importName: 'WebApiConfiguration'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return generateQueryServiceFile(endpointInfos, generatorOptions);
|
|
23
|
+
}
|
|
24
|
+
}
|