@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,353 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
4
|
+
import { OpenApiService } from './openapi-service';
|
|
5
|
+
import { GroupService, type GroupConfig } from './group-service';
|
|
6
|
+
import { FileWriterService, type FileWriteResult } from './file-writer-service';
|
|
7
|
+
export interface EndpointCacheKey {
|
|
8
|
+
operationName: string;
|
|
9
|
+
queryKeyName: string;
|
|
10
|
+
groupKey: string;
|
|
11
|
+
}
|
|
12
|
+
import { OpenApiParserService } from './openapi-parser-service';
|
|
13
|
+
import { generateCacheKeysFile } from '../generators/cache-keys-generator';
|
|
14
|
+
import { generateCommonTypesFile } from '../generators/common-types-generator';
|
|
15
|
+
import { generateComponentSchemaFile } from '../generators/component-schema-generator';
|
|
16
|
+
import { generateDoNotModifyFile } from '../generators/do-not-modify-generator';
|
|
17
|
+
import type { GenerationOptions, CommonOptions } from '../types';
|
|
18
|
+
import { ApiCodeGenerator } from './api-code-generator';
|
|
19
|
+
import { generateUtilsFile } from '../generators/utils-generator';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 統一代碼生成器選項
|
|
23
|
+
*/
|
|
24
|
+
export interface UnifiedGenerationOptions extends CommonOptions {
|
|
25
|
+
outputFiles: GroupConfig;
|
|
26
|
+
remoteFile?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 統一代碼生成器結果
|
|
31
|
+
*/
|
|
32
|
+
export interface UnifiedGenerationResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
writtenFiles: FileWriteResult[];
|
|
35
|
+
errors: Error[];
|
|
36
|
+
generatedGroups: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 統一代碼生成器
|
|
41
|
+
*
|
|
42
|
+
* 設計理念:
|
|
43
|
+
* - 統一管理所有生成流程
|
|
44
|
+
* - 內建 schema 處理和存儲
|
|
45
|
+
* - 提供準備、生成、發佈的分階段操作
|
|
46
|
+
* - 避免重複初始化和處理
|
|
47
|
+
*
|
|
48
|
+
* 使用方式:
|
|
49
|
+
* 1. prepare() - 準備階段:解析 schema、初始化服務
|
|
50
|
+
* 2. generateContent() - 生成階段:產生所有內容但不寫檔
|
|
51
|
+
* 3. release() - 發佈階段:統一寫入所有檔案
|
|
52
|
+
*/
|
|
53
|
+
export class UnifiedCodeGenerator {
|
|
54
|
+
private _options: UnifiedGenerationOptions;
|
|
55
|
+
|
|
56
|
+
private openApiService = new OpenApiService();
|
|
57
|
+
private groupService = new GroupService();
|
|
58
|
+
private fileWriterService = new FileWriterService();
|
|
59
|
+
|
|
60
|
+
// 內部狀態存儲
|
|
61
|
+
private openApiDoc: OpenAPIV3.Document | null = null;
|
|
62
|
+
private parserService: OpenApiParserService | null = null;
|
|
63
|
+
private schemaInterfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration> = {};
|
|
64
|
+
private allEndpointCacheKeys: EndpointCacheKey[] = [];
|
|
65
|
+
private actualSchemaFile: string = '';
|
|
66
|
+
|
|
67
|
+
// 生成內容存儲
|
|
68
|
+
private generatedContent: {
|
|
69
|
+
groups: Array<{
|
|
70
|
+
groupKey: string;
|
|
71
|
+
outputPath: string;
|
|
72
|
+
content: any;
|
|
73
|
+
}>;
|
|
74
|
+
cacheKeys: string | null;
|
|
75
|
+
commonTypes: string;
|
|
76
|
+
componentSchema: string;
|
|
77
|
+
doNotModify: string;
|
|
78
|
+
utils: string;
|
|
79
|
+
} = {
|
|
80
|
+
groups: [],
|
|
81
|
+
cacheKeys: null,
|
|
82
|
+
commonTypes: '',
|
|
83
|
+
componentSchema: '',
|
|
84
|
+
doNotModify: '',
|
|
85
|
+
utils: ''
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
constructor(options: UnifiedGenerationOptions) {
|
|
90
|
+
this._options = options;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 一次性生成(整合所有階段)
|
|
97
|
+
*/
|
|
98
|
+
async generateAll(): Promise<UnifiedGenerationResult> {
|
|
99
|
+
await this.prepare();
|
|
100
|
+
|
|
101
|
+
// 生成各API文件
|
|
102
|
+
await this.generateApi();
|
|
103
|
+
// await this.generateQuery();
|
|
104
|
+
|
|
105
|
+
// 生成共用
|
|
106
|
+
this.generateCacheKeysContent()
|
|
107
|
+
this.generateCommonTypesContent()
|
|
108
|
+
this.generateSchemaContent()
|
|
109
|
+
this.generateUtilsContent()
|
|
110
|
+
this.generateDoNotModifyContent()
|
|
111
|
+
|
|
112
|
+
return await this.release();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 準備階段:解析 schema 並初始化所有服務
|
|
119
|
+
*/
|
|
120
|
+
async prepare(): Promise<void> {
|
|
121
|
+
// console.log('UnifiedCodeGenerator: 準備階段開始...');
|
|
122
|
+
|
|
123
|
+
// 步驟 1: 解析實際的 schema 檔案路徑
|
|
124
|
+
this.actualSchemaFile = this._options.schemaFile;
|
|
125
|
+
if (this._options.remoteFile) {
|
|
126
|
+
this.actualSchemaFile = await this.openApiService.downloadSchema(
|
|
127
|
+
this._options.remoteFile,
|
|
128
|
+
this._options.schemaFile
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 步驟 2: 獲取 OpenAPI 文檔(只初始化一次)
|
|
133
|
+
this.openApiDoc = await this.openApiService.getDocument(
|
|
134
|
+
this.actualSchemaFile,
|
|
135
|
+
this._options.httpResolverOptions
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// 步驟 3: 初始化解析器服務並處理 schema
|
|
139
|
+
this.parserService = new OpenApiParserService(this.openApiDoc, this._options);
|
|
140
|
+
this.parserService.initialize();
|
|
141
|
+
|
|
142
|
+
// 步驟 4: 提取並儲存 schema interfaces
|
|
143
|
+
const apiGen = this.parserService.getApiGenerator();
|
|
144
|
+
this.schemaInterfaces = apiGen.aliases.reduce<Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>>((curr, alias) => {
|
|
145
|
+
if (ts.isInterfaceDeclaration(alias) || ts.isTypeAliasDeclaration(alias)) {
|
|
146
|
+
const name = alias.name.text;
|
|
147
|
+
return {
|
|
148
|
+
...curr,
|
|
149
|
+
[name]: alias,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return curr;
|
|
153
|
+
}, {});
|
|
154
|
+
|
|
155
|
+
// console.log('UnifiedCodeGenerator: 準備階段完成');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 生成階段:產生所有內容但不寫檔
|
|
162
|
+
*/
|
|
163
|
+
async generateApi(): Promise<void> {
|
|
164
|
+
if (!this.openApiDoc || !this.parserService) {
|
|
165
|
+
throw new Error('請先調用 prepare() 方法');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// console.log('UnifiedCodeGenerator: 內容生成階段開始...');
|
|
169
|
+
|
|
170
|
+
// 獲取所有 API 接口Path並分組
|
|
171
|
+
const paths = this.openApiService.getPaths(this.openApiDoc);
|
|
172
|
+
const groupInfos = this.groupService.groupPaths(paths, this._options.outputFiles);
|
|
173
|
+
|
|
174
|
+
// 為每個群組生成內容
|
|
175
|
+
for (const groupInfo of Object.values(groupInfos)) {
|
|
176
|
+
try {
|
|
177
|
+
const groupContent = await this.generateApiGroupContent(
|
|
178
|
+
this._options,
|
|
179
|
+
groupInfo
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// 檢查群組是否有任何有效的 endpoint
|
|
183
|
+
if (groupContent.operationNames.length > 0) {
|
|
184
|
+
this.generatedContent.groups.push({
|
|
185
|
+
groupKey: groupInfo.groupKey,
|
|
186
|
+
outputPath: groupInfo.outputPath,
|
|
187
|
+
content: groupContent
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// 如果沒有任何 endpoint,則跳過此群組,不創建資料夾
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(`群組 ${groupInfo.groupKey} 生成失敗: ${error}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// console.log('UnifiedCodeGenerator: 內容生成階段完成');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 生成 cache keys
|
|
202
|
+
*/
|
|
203
|
+
private async generateCacheKeysContent(): Promise<void> {
|
|
204
|
+
this.generatedContent.cacheKeys = generateCacheKeysFile(this.allEndpointCacheKeys);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 生成 common types
|
|
209
|
+
*/
|
|
210
|
+
private async generateCommonTypesContent(): Promise<void> {
|
|
211
|
+
this.generatedContent.commonTypes = generateCommonTypesFile();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 生成Schema
|
|
216
|
+
*/
|
|
217
|
+
private async generateSchemaContent(): Promise<void> {
|
|
218
|
+
this.generatedContent.componentSchema = generateComponentSchemaFile(this.schemaInterfaces);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 生成 DO_NOT_MODIFY.md
|
|
223
|
+
*/
|
|
224
|
+
private async generateDoNotModifyContent(): Promise<void> {
|
|
225
|
+
this.generatedContent.doNotModify = generateDoNotModifyFile();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 生成 Utils Function
|
|
231
|
+
*/
|
|
232
|
+
private async generateUtilsContent(): Promise<void> {
|
|
233
|
+
this.generatedContent.utils = generateUtilsFile();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 發佈階段:統一寫入所有檔案
|
|
241
|
+
*/
|
|
242
|
+
async release(): Promise<UnifiedGenerationResult> {
|
|
243
|
+
const results: FileWriteResult[] = [];
|
|
244
|
+
const errors: Error[] = [];
|
|
245
|
+
const generatedGroups: string[] = [];
|
|
246
|
+
|
|
247
|
+
// console.log('UnifiedCodeGenerator: 發佈階段開始...');
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// 寫入群組檔案
|
|
251
|
+
for (const group of this.generatedContent.groups) {
|
|
252
|
+
try {
|
|
253
|
+
if (group.content?.files) {
|
|
254
|
+
const groupOutputDir = path.dirname(group.outputPath);
|
|
255
|
+
const groupResults = await this.fileWriterService.writeGroupFiles(
|
|
256
|
+
groupOutputDir,
|
|
257
|
+
{
|
|
258
|
+
types: group.content.files.types,
|
|
259
|
+
apiService: group.content.files.apiService,
|
|
260
|
+
queryService: group.content.files.queryService,
|
|
261
|
+
index: group.content.files.index
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
results.push(...groupResults);
|
|
265
|
+
generatedGroups.push(group.groupKey);
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
errors.push(new Error(`寫入群組 ${group.groupKey} 失敗: ${error}`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 寫入共用檔案
|
|
273
|
+
const outputDir = this.generatedContent.groups[0] ?
|
|
274
|
+
path.dirname(path.dirname(this.generatedContent.groups[0].outputPath)) :
|
|
275
|
+
'./generated';
|
|
276
|
+
|
|
277
|
+
// 寫入共用檔案 (包含 DO_NOT_MODIFY.md)
|
|
278
|
+
if (this.generatedContent.cacheKeys || this.generatedContent.commonTypes || this.generatedContent.doNotModify || this.generatedContent.utils) {
|
|
279
|
+
const sharedResults = await this.fileWriterService.writeSharedFiles(
|
|
280
|
+
outputDir,
|
|
281
|
+
{
|
|
282
|
+
cacheKeys: this.generatedContent.cacheKeys || undefined,
|
|
283
|
+
commonTypes: this.generatedContent.commonTypes || undefined,
|
|
284
|
+
doNotModify: this.generatedContent.doNotModify || undefined,
|
|
285
|
+
utils: this.generatedContent.utils || undefined
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
results.push(...sharedResults);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 寫入 component schema
|
|
292
|
+
if (this.generatedContent.componentSchema) {
|
|
293
|
+
const schemaResults = await this.fileWriterService.writeSchemaFile(
|
|
294
|
+
outputDir,
|
|
295
|
+
this.generatedContent.componentSchema
|
|
296
|
+
);
|
|
297
|
+
results.push(...schemaResults);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
} catch (error) {
|
|
301
|
+
errors.push(error as Error);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// console.log('UnifiedCodeGenerator: 發佈階段完成');
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
success: errors.length === 0,
|
|
308
|
+
writtenFiles: results,
|
|
309
|
+
errors,
|
|
310
|
+
generatedGroups
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 為單一群組生成內容
|
|
317
|
+
*/
|
|
318
|
+
private async generateApiGroupContent(
|
|
319
|
+
options: UnifiedGenerationOptions,
|
|
320
|
+
groupInfo: { groupKey: string; paths: string[]; outputPath: string }
|
|
321
|
+
): Promise<any> {
|
|
322
|
+
const { outputFiles, ...commonConfig } = options;
|
|
323
|
+
|
|
324
|
+
// 建立群組特定的生成選項
|
|
325
|
+
const groupOptions: GenerationOptions = {
|
|
326
|
+
...commonConfig,
|
|
327
|
+
schemaFile: this.actualSchemaFile,
|
|
328
|
+
outputFile: groupInfo.outputPath,
|
|
329
|
+
sharedTypesFile: `${outputFiles.outputDir}/common-types.ts`,
|
|
330
|
+
filterEndpoints: this.groupService.createGroupFilter(groupInfo.groupKey, outputFiles),
|
|
331
|
+
queryMatch: outputFiles.queryMatch,
|
|
332
|
+
groupKey: groupInfo.groupKey,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// 使用新的 ApiCodeGenerator 生成程式碼,重用已處理的 v3Doc
|
|
336
|
+
if (!this.openApiDoc || !this.parserService) {
|
|
337
|
+
throw new Error('OpenAPI 文檔未初始化,請先調用 prepare()');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const apiGenerator = new ApiCodeGenerator(this.parserService, groupOptions);
|
|
341
|
+
const result = await apiGenerator.generate();
|
|
342
|
+
|
|
343
|
+
// 提取並儲存快取鍵
|
|
344
|
+
if (result.files && 'allEndpointCacheKeys' in result.files) {
|
|
345
|
+
const cacheKeys = result.files.allEndpointCacheKeys as EndpointCacheKey[];
|
|
346
|
+
this.allEndpointCacheKeys.push(...cacheKeys);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type SwaggerParser from '@apidevtools/swagger-parser';
|
|
2
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
// 重新匯出服務相關類型
|
|
6
|
+
export type {
|
|
7
|
+
GroupConfig,
|
|
8
|
+
GroupInfo
|
|
9
|
+
} from './services/group-service';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
FileWriteResult
|
|
13
|
+
} from './services/file-writer-service';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
EndpointCacheKey
|
|
17
|
+
} from './services/unified-code-generator';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
UnifiedGenerationOptions as EndpointGenerationOptions,
|
|
21
|
+
UnifiedGenerationResult as EndpointGenerationResult
|
|
22
|
+
} from './services/unified-code-generator';
|
|
23
|
+
|
|
24
|
+
export type OperationDefinition = {
|
|
25
|
+
path: string;
|
|
26
|
+
verb: (typeof operationKeys)[number];
|
|
27
|
+
pathItem: OpenAPIV3.PathItemObject;
|
|
28
|
+
operation: OpenAPIV3.OperationObject;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ParameterDefinition = OpenAPIV3.ParameterObject;
|
|
32
|
+
|
|
33
|
+
type Require<T, K extends keyof T> = { [k in K]-?: NonNullable<T[k]> } & Omit<T, K>;
|
|
34
|
+
type Optional<T, K extends keyof T> = { [k in K]?: NonNullable<T[k]> } & Omit<T, K>;
|
|
35
|
+
type Id<T> = { [K in keyof T]: T[K] } & {};
|
|
36
|
+
type AtLeastOneKey<T> = {
|
|
37
|
+
[K in keyof T]-?: Pick<T, K> & Partial<T>;
|
|
38
|
+
}[keyof T];
|
|
39
|
+
|
|
40
|
+
export const operationKeys = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const;
|
|
41
|
+
|
|
42
|
+
export type GenerationOptions = Id<
|
|
43
|
+
CommonOptions &
|
|
44
|
+
Optional<OutputFileOptions, 'outputFile'> & {
|
|
45
|
+
isDataResponse?(
|
|
46
|
+
code: string,
|
|
47
|
+
includeDefault: boolean,
|
|
48
|
+
response: OpenAPIV3.ResponseObject,
|
|
49
|
+
allResponses: OpenAPIV3.ResponsesObject
|
|
50
|
+
): boolean;
|
|
51
|
+
}
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
export interface CommonOptions {
|
|
55
|
+
/**
|
|
56
|
+
* local schema file path (only supports local files)
|
|
57
|
+
*/
|
|
58
|
+
schemaFile: string;
|
|
59
|
+
/**
|
|
60
|
+
* remote schema file URL (when provided, will download to schemaFile path)
|
|
61
|
+
*/
|
|
62
|
+
remoteFile?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for WebApiConfiguration import
|
|
65
|
+
* defaults to { file: "@core/api/web-api-configuration", importName: "WebApiConfiguration" }
|
|
66
|
+
*/
|
|
67
|
+
apiConfiguration?: {
|
|
68
|
+
file: string;
|
|
69
|
+
importName: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* HttpClient for WebApiConfiguration import
|
|
73
|
+
* defaults to { file: "@core/httpClient/webapi/webapi-http-client.providers", importName: "WEBAPI_HTTP_CLIENT" }
|
|
74
|
+
*/
|
|
75
|
+
httpClient?: {
|
|
76
|
+
file: string;
|
|
77
|
+
importName: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* defaults to "enhancedApi"
|
|
81
|
+
*/
|
|
82
|
+
exportName?: string;
|
|
83
|
+
/**
|
|
84
|
+
* defaults to "Req"
|
|
85
|
+
*/
|
|
86
|
+
argSuffix?: string;
|
|
87
|
+
/**
|
|
88
|
+
* defaults to "Res"
|
|
89
|
+
*/
|
|
90
|
+
responseSuffix?: string;
|
|
91
|
+
/**
|
|
92
|
+
* defaults to empty
|
|
93
|
+
*/
|
|
94
|
+
operationNameSuffix?: string;
|
|
95
|
+
/**
|
|
96
|
+
* defaults to `false`
|
|
97
|
+
* `true` will generate hooks for queries and mutations, but no lazyQueries
|
|
98
|
+
*/
|
|
99
|
+
hooks?: boolean | { queries: boolean; lazyQueries: boolean; mutations: boolean };
|
|
100
|
+
/**
|
|
101
|
+
* defaults to false
|
|
102
|
+
* `true` will generate a union type for `undefined` properties like: `{ id?: string | undefined }` instead of `{ id?: string }`
|
|
103
|
+
*/
|
|
104
|
+
unionUndefined?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* defaults to false
|
|
107
|
+
* `true` will result in all generated endpoints having `providesTags`/`invalidatesTags` declarations for the `tags` of their respective operation definition
|
|
108
|
+
* @see https://redux-toolkit.js.org/rtk-query/usage/code-generation for more information
|
|
109
|
+
*/
|
|
110
|
+
tag?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* defaults to false
|
|
113
|
+
* `true` will add `encodeURIComponent` to the generated path parameters
|
|
114
|
+
*/
|
|
115
|
+
encodePathParams?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* defaults to false
|
|
118
|
+
* `true` will add `encodeURIComponent` to the generated query parameters
|
|
119
|
+
*/
|
|
120
|
+
encodeQueryParams?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* defaults to false
|
|
123
|
+
* `true` will "flatten" the arg so that you can do things like `useGetEntityById(1)` instead of `useGetEntityById({ entityId: 1 })`
|
|
124
|
+
*/
|
|
125
|
+
flattenArg?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* default to false
|
|
128
|
+
* If set to `true`, the default response type will be included in the generated code for all endpoints.
|
|
129
|
+
* @see https://swagger.io/docs/specification/describing-responses/#default
|
|
130
|
+
*/
|
|
131
|
+
includeDefault?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* default to false
|
|
134
|
+
* `true` will not generate separate types for read-only and write-only properties.
|
|
135
|
+
*/
|
|
136
|
+
mergeReadWriteOnly?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
*
|
|
139
|
+
* HTTPResolverOptions object that is passed to the SwaggerParser bundle function.
|
|
140
|
+
*/
|
|
141
|
+
httpResolverOptions?: SwaggerParser.HTTPResolverOptions;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* defaults to undefined
|
|
145
|
+
* If present the given file will be used as prettier config when formatting the generated code. If undefined the default prettier config
|
|
146
|
+
* resolution mechanism will be used.
|
|
147
|
+
*/
|
|
148
|
+
prettierConfigFile?: string;
|
|
149
|
+
/**
|
|
150
|
+
* defaults to "@acrool/react-fetcher"
|
|
151
|
+
* File path for importing IRestFulEndpointsQueryReturn type
|
|
152
|
+
*/
|
|
153
|
+
endpointsQueryReturnTypeFile?: string;
|
|
154
|
+
/**
|
|
155
|
+
* defaults to 60 (seconds)
|
|
156
|
+
* Number of seconds to wait before refetching data when component mounts or args change
|
|
157
|
+
*/
|
|
158
|
+
refetchOnMountOrArgChange?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type TextMatcher = string | RegExp | (string | RegExp)[];
|
|
162
|
+
|
|
163
|
+
export type EndpointMatcherFunction = (operationName: string, operationDefinition: OperationDefinition) => boolean;
|
|
164
|
+
|
|
165
|
+
export type EndpointMatcher = TextMatcher | EndpointMatcherFunction;
|
|
166
|
+
|
|
167
|
+
export type ParameterMatcherFunction = (parameterName: string, parameterDefinition: ParameterDefinition) => boolean;
|
|
168
|
+
|
|
169
|
+
export type ParameterMatcher = TextMatcher | ParameterMatcherFunction;
|
|
170
|
+
|
|
171
|
+
export interface OutputFileOptions extends Partial<CommonOptions> {
|
|
172
|
+
outputFile: string;
|
|
173
|
+
filterEndpoints?: EndpointMatcher;
|
|
174
|
+
endpointOverrides?: EndpointOverrides[];
|
|
175
|
+
queryMatch?: (method: string, path: string) => boolean;
|
|
176
|
+
/**
|
|
177
|
+
* defaults to false
|
|
178
|
+
* If passed as true it will generate TS enums instead of union of strings
|
|
179
|
+
*/
|
|
180
|
+
useEnumType?: boolean;
|
|
181
|
+
sharedTypesFile?: string;
|
|
182
|
+
/**
|
|
183
|
+
* groupKey for service class naming, e.g., "room" -> "RoomService"
|
|
184
|
+
*/
|
|
185
|
+
groupKey?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type EndpointOverrides = {
|
|
189
|
+
pattern: EndpointMatcher;
|
|
190
|
+
} & AtLeastOneKey<{
|
|
191
|
+
type: 'mutation' | 'query';
|
|
192
|
+
parameterFilter: ParameterMatcher;
|
|
193
|
+
}>;
|
|
194
|
+
|
|
195
|
+
export type OutputFilesConfig = {
|
|
196
|
+
groupKeyMatch: (path: string) => string;
|
|
197
|
+
outputDir: string;
|
|
198
|
+
queryMatch?: (method: string, path: string) => boolean;
|
|
199
|
+
filterEndpoint?: (operationName: string, path: string, groupKey: string) => boolean;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type ConfigFile =
|
|
203
|
+
| Id<Require<CommonOptions & OutputFileOptions, 'outputFile'>>
|
|
204
|
+
| Id<
|
|
205
|
+
Omit<CommonOptions, 'outputFile'> & {
|
|
206
|
+
// outputFiles: { [outputFile: string]: Omit<OutputFileOptions, 'outputFile'> };
|
|
207
|
+
outputFiles: OutputFilesConfig
|
|
208
|
+
}
|
|
209
|
+
>;
|
|
210
|
+
|
|
211
|
+
export type GenerateApiResult = {
|
|
212
|
+
operationNames: string[];
|
|
213
|
+
files: {
|
|
214
|
+
types: string;
|
|
215
|
+
apiService: string;
|
|
216
|
+
queryService: string;
|
|
217
|
+
index: string;
|
|
218
|
+
commonTypes?: string;
|
|
219
|
+
cacheKeys?: string;
|
|
220
|
+
componentSchema?: string;
|
|
221
|
+
allEndpointCacheKeys?: Array<{
|
|
222
|
+
operationName: string
|
|
223
|
+
queryKeyName: string
|
|
224
|
+
groupKey: string
|
|
225
|
+
}>;
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
export type QueryArgDefinition = {
|
|
232
|
+
name: string;
|
|
233
|
+
originalName: string;
|
|
234
|
+
type: ts.TypeNode;
|
|
235
|
+
required?: boolean;
|
|
236
|
+
param?: OpenAPIV3.ParameterObject;
|
|
237
|
+
} & (
|
|
238
|
+
| {
|
|
239
|
+
origin: 'param';
|
|
240
|
+
param: OpenAPIV3.ParameterObject;
|
|
241
|
+
}
|
|
242
|
+
| {
|
|
243
|
+
origin: 'body';
|
|
244
|
+
body: OpenAPIV3.RequestBodyObject;
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
export type QueryArgDefinitions = Record<string, QueryArgDefinition>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 確保目錄存在的函數
|
|
6
|
+
* @param filePath
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureDirectoryExists(filePath: string) {
|
|
9
|
+
const dirname = path.dirname(filePath);
|
|
10
|
+
if (!fs.existsSync(dirname)) {
|
|
11
|
+
await fs.promises.mkdir(dirname, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 檢查檔案是否存在的函數
|
|
17
|
+
* @param filePath
|
|
18
|
+
*/
|
|
19
|
+
export function fileExists(filePath: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
return fs.statSync(filePath).isFile();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 獲取資料夾名稱並轉換為 API 名稱
|
|
29
|
+
* @param dirPath
|
|
30
|
+
*/
|
|
31
|
+
export function getApiNameFromDir(dirPath: string): string {
|
|
32
|
+
const dirName = path.basename(dirPath);
|
|
33
|
+
return `${dirName}Api`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 確保基礎文件存在的函數
|
|
41
|
+
* @param outputDir
|
|
42
|
+
* @param operationNames
|
|
43
|
+
*/
|
|
44
|
+
export async function ensureBaseFilesExist(outputDir: string, operationNames: string[]) {
|
|
45
|
+
const enhanceEndpointsPath = path.join(outputDir, 'enhanceEndpoints.ts');
|
|
46
|
+
const indexPath = path.join(outputDir, 'index.ts');
|
|
47
|
+
|
|
48
|
+
// 如果 enhanceEndpoints.ts 不存在,創建它
|
|
49
|
+
if (!fileExists(enhanceEndpointsPath)) {
|
|
50
|
+
// 生成操作名稱的字符串
|
|
51
|
+
const operationNamesString = operationNames
|
|
52
|
+
.map(name => ` ${name}: {},`)
|
|
53
|
+
.join('\n');
|
|
54
|
+
|
|
55
|
+
const enhanceEndpointsContent = `import api from './query.service';
|
|
56
|
+
|
|
57
|
+
const enhancedApi = api.enhanceEndpoints({
|
|
58
|
+
endpoints: {
|
|
59
|
+
${operationNamesString}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default enhancedApi;
|
|
64
|
+
`;
|
|
65
|
+
await fs.promises.writeFile(enhanceEndpointsPath, enhanceEndpointsContent, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
// 如果文件已存在,不做任何修改
|
|
68
|
+
|
|
69
|
+
// 如果 index.ts 不存在,創建它
|
|
70
|
+
if (!fileExists(indexPath)) {
|
|
71
|
+
const indexContent = `export * from './query.service';
|
|
72
|
+
`;
|
|
73
|
+
await fs.promises.writeFile(indexPath, indexContent, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isValidUrl } from './isValidUrl';
|
|
4
|
+
|
|
5
|
+
export async function downloadSchemaFile(remoteFile: string, targetPath: string): Promise<string> {
|
|
6
|
+
// 如果不是網址,拋出錯誤
|
|
7
|
+
if (!isValidUrl(remoteFile)) {
|
|
8
|
+
throw new Error(`remoteFile must be a valid URL: ${remoteFile}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// 確保目錄存在
|
|
13
|
+
const dir = path.dirname(targetPath);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 下載檔案
|
|
19
|
+
const response = await fetch(remoteFile);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Failed to download schema from ${remoteFile}: ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = await response.text();
|
|
25
|
+
await fs.promises.writeFile(targetPath, content, 'utf-8');
|
|
26
|
+
|
|
27
|
+
console.log(`Schema downloaded from ${remoteFile} to ${targetPath}`);
|
|
28
|
+
return targetPath;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`Error downloading schema from ${remoteFile}:`, error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|