@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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 產生 CommonTypeFile
|
|
3
|
+
*/
|
|
4
|
+
export function generateCommonTypesFile() {
|
|
5
|
+
|
|
6
|
+
return `/* eslint-disable */
|
|
7
|
+
// [Warning] Generated automatically - do not edit manually
|
|
8
|
+
|
|
9
|
+
export interface RequestOptions {
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
observe?: 'body' | 'events' | 'response';
|
|
12
|
+
responseType?: 'arraybuffer' | 'blob' | 'json' | 'text';
|
|
13
|
+
reportProgress?: boolean;
|
|
14
|
+
withCredentials?: boolean;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueryConfig {
|
|
19
|
+
refetchOnMountOrArgChange?: boolean|number,
|
|
20
|
+
keepUnusedDataFor?: number,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type IRestFulEndpointsQueryReturn<TVariables> = TVariables extends void ?
|
|
24
|
+
void | {fetchOptions?: RequestOptions;config?: QueryConfig;}:
|
|
25
|
+
{
|
|
26
|
+
variables: TVariables;
|
|
27
|
+
fetchOptions?: RequestOptions;
|
|
28
|
+
config?: QueryConfig;
|
|
29
|
+
};
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 產生 component-schema.ts 內容
|
|
6
|
+
* @param interfaces
|
|
7
|
+
*/
|
|
8
|
+
export function generateComponentSchemaFile(interfaces: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>) {
|
|
9
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
10
|
+
|
|
11
|
+
const resultFile = ts.createSourceFile(
|
|
12
|
+
'component-schema.ts',
|
|
13
|
+
'',
|
|
14
|
+
ts.ScriptTarget.Latest,
|
|
15
|
+
false,
|
|
16
|
+
ts.ScriptKind.TS
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// 分析接口內容以找出需要從 shared-types 導入的類型
|
|
20
|
+
return `/* eslint-disable */
|
|
21
|
+
// [Warning] Generated automatically - do not edit manually
|
|
22
|
+
|
|
23
|
+
${Object.values(interfaces).map(i => printer.printNode(ts.EmitHint.Unspecified, i, resultFile)).join('\n')}
|
|
24
|
+
`
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 產生 DO_NOT_MODIFY.md 說明檔案
|
|
3
|
+
*
|
|
4
|
+
* 此檔案用於提醒開發者不要修改產生器生成的程式碼
|
|
5
|
+
*/
|
|
6
|
+
export function generateDoNotModifyFile(): string {
|
|
7
|
+
return `# 請勿修改此資料夾
|
|
8
|
+
|
|
9
|
+
⚠️ **重要提醒:請勿修改此資料夾中的任何檔案**
|
|
10
|
+
|
|
11
|
+
此資料夾中的所有檔案都是透過程式碼產生器自動產生的。任何手動修改的內容在下次重新產生時都將會被覆蓋。
|
|
12
|
+
|
|
13
|
+
## 如何修改這些檔案?
|
|
14
|
+
|
|
15
|
+
如果您需要修改這些檔案的內容,請:
|
|
16
|
+
|
|
17
|
+
1. 修改相對應的設定檔案或模板
|
|
18
|
+
2. 重新執行程式碼產生器
|
|
19
|
+
3. 讓產生器自動更新這些檔案
|
|
20
|
+
|
|
21
|
+
## 產生器相關資訊
|
|
22
|
+
|
|
23
|
+
這些檔案是由 @bitstack/ng-query-codegen-openapi 產生器所建立。
|
|
24
|
+
|
|
25
|
+
如有疑問,請參考專案文件或聯繫開發團隊。
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GenerationOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
export function generateIndexFile(groupKey: string, options: GenerationOptions) {
|
|
4
|
+
const { groupKey: optionsGroupKey } = options;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
return `export * from './types';
|
|
8
|
+
export * from './api.service';
|
|
9
|
+
export * from './query.service';
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { GenerationOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
export function generateQueryServiceFile(endpointInfos: Array<{
|
|
4
|
+
operationName: string;
|
|
5
|
+
argTypeName: string;
|
|
6
|
+
responseTypeName: string;
|
|
7
|
+
isQuery: boolean;
|
|
8
|
+
verb: string;
|
|
9
|
+
path: string;
|
|
10
|
+
queryKeyName: string;
|
|
11
|
+
queryParams: any[];
|
|
12
|
+
pathParams: any[];
|
|
13
|
+
isVoidArg: boolean;
|
|
14
|
+
summary: string;
|
|
15
|
+
}>, options: GenerationOptions) {
|
|
16
|
+
|
|
17
|
+
const { groupKey, refetchOnMountOrArgChange = 60 } = options;
|
|
18
|
+
|
|
19
|
+
// 確定服務名稱 - 使用 groupKey 如果有提供的話
|
|
20
|
+
const queryServiceName = groupKey ?
|
|
21
|
+
`${groupKey.charAt(0).toUpperCase() + groupKey.slice(1)}QueryService` :
|
|
22
|
+
'QueryService';
|
|
23
|
+
|
|
24
|
+
const apiServiceName = groupKey ?
|
|
25
|
+
`${groupKey.charAt(0).toUpperCase() + groupKey.slice(1)}ApiService` :
|
|
26
|
+
'ApiService';
|
|
27
|
+
|
|
28
|
+
// 分別處理 Query 和 Mutation 方法
|
|
29
|
+
const queryMethods = endpointInfos
|
|
30
|
+
.filter(info => info.isQuery)
|
|
31
|
+
.map(info => `
|
|
32
|
+
/**
|
|
33
|
+
* ${info.summary}
|
|
34
|
+
*/
|
|
35
|
+
${info.operationName}Query(${info.argTypeName !== 'VoidApiArg' ? `args: IRestFulEndpointsQueryReturn<${info.argTypeName}>` : ''}): Observable<QueryState<${info.responseTypeName}>> {
|
|
36
|
+
return this.ntkQuery.query<${info.responseTypeName}, ${info.argTypeName !== 'VoidApiArg' ? `IRestFulEndpointsQueryReturn<${info.argTypeName}>` : 'void'}>({
|
|
37
|
+
queryKey: [ECacheKeys.${info.queryKeyName}${info.argTypeName !== 'VoidApiArg' ? ', args.variables' : ''}],
|
|
38
|
+
queryFn: () => {
|
|
39
|
+
return this.apiService.${info.operationName}(${info.argTypeName !== 'VoidApiArg' ? 'args' : ''});
|
|
40
|
+
},
|
|
41
|
+
fetchOptions: args?.fetchOptions,
|
|
42
|
+
config: args?.config,
|
|
43
|
+
})${info.argTypeName !== 'VoidApiArg' ? '(args)' : '()'};
|
|
44
|
+
}`).join('');
|
|
45
|
+
|
|
46
|
+
const mutationMethods = endpointInfos
|
|
47
|
+
.filter(info => !info.isQuery)
|
|
48
|
+
.map(info => `
|
|
49
|
+
/**
|
|
50
|
+
* ${info.summary}
|
|
51
|
+
*/
|
|
52
|
+
${info.operationName}Mutation(): MutationResponse<${info.responseTypeName}, {
|
|
53
|
+
variables: ${info.argTypeName !== 'VoidApiArg' ? info.argTypeName : 'void'};
|
|
54
|
+
fetchOptions?: RequestOptions;
|
|
55
|
+
}, HttpErrorResponse> {
|
|
56
|
+
return this.ntkQuery.mutation<${info.responseTypeName}, ${info.argTypeName !== 'VoidApiArg' ? `IRestFulEndpointsQueryReturn<${info.argTypeName}>` : 'void'}, HttpErrorResponse>({
|
|
57
|
+
endpointName: '${info.operationName}',
|
|
58
|
+
mutationFn: (${info.argTypeName !== 'VoidApiArg' ? 'args' : ''}) => {
|
|
59
|
+
return this.apiService.${info.operationName}(${info.argTypeName !== 'VoidApiArg' ? 'args' : ''});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}`).join('');
|
|
63
|
+
|
|
64
|
+
const lazyQueryMethods = endpointInfos
|
|
65
|
+
.filter(info => info.isQuery)
|
|
66
|
+
.map(info => `
|
|
67
|
+
/**
|
|
68
|
+
* ${info.summary}
|
|
69
|
+
*/
|
|
70
|
+
${info.operationName}LazyQuery(): {
|
|
71
|
+
trigger: (${info.argTypeName !== 'VoidApiArg' ? `args: IRestFulEndpointsQueryReturn<${info.argTypeName}>, ` : ''}options?: { skipCache?: boolean }) => Observable<${info.responseTypeName}>
|
|
72
|
+
} {
|
|
73
|
+
return {
|
|
74
|
+
trigger: (${info.argTypeName !== 'VoidApiArg' ? 'args, ' : ''}options = {}) => {
|
|
75
|
+
const { skipCache = true } = options;
|
|
76
|
+
if (!skipCache) {
|
|
77
|
+
const cachedResult = this.ntkQuery.getQueryCache<${info.responseTypeName}>([ECacheKeys.${info.queryKeyName}${info.argTypeName !== 'VoidApiArg' ? ', args.variables' : ''}]);
|
|
78
|
+
if (cachedResult) {
|
|
79
|
+
return cachedResult;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return this.apiService.${info.operationName}(${info.argTypeName !== 'VoidApiArg' ? 'args' : ''});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}`).join('');
|
|
86
|
+
|
|
87
|
+
return `/* eslint-disable */
|
|
88
|
+
// [Warning] Generated automatically - do not edit manually
|
|
89
|
+
|
|
90
|
+
import {Injectable, inject} from '@angular/core';
|
|
91
|
+
import {Observable} from 'rxjs';
|
|
92
|
+
import {HttpErrorResponse} from '@angular/common/http';
|
|
93
|
+
import {NtkQueryService, MutationState, QueryState, MutationResponse} from '@core/ntk-query';
|
|
94
|
+
import {ECacheKeys} from '../cache-keys';
|
|
95
|
+
import {${apiServiceName}} from './api.service';
|
|
96
|
+
import {IRestFulEndpointsQueryReturn, RequestOptions} from '../common-types';
|
|
97
|
+
import {${endpointInfos.map(info => ` ${info.argTypeName}, ${info.responseTypeName}`).join(',')}} from './types';
|
|
98
|
+
|
|
99
|
+
@Injectable({
|
|
100
|
+
providedIn: 'root',
|
|
101
|
+
})
|
|
102
|
+
export class ${queryServiceName} {
|
|
103
|
+
private apiService = inject(${apiServiceName});
|
|
104
|
+
private ntkQuery = inject(NtkQueryService);
|
|
105
|
+
${queryMethods}${mutationMethods}${lazyQueryMethods}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { GenerationOptions } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface EndpointInfo {
|
|
5
|
+
operationName: string;
|
|
6
|
+
argTypeName: string;
|
|
7
|
+
responseTypeName: string;
|
|
8
|
+
isQuery: boolean;
|
|
9
|
+
verb: string;
|
|
10
|
+
path: string;
|
|
11
|
+
queryKeyName: string;
|
|
12
|
+
queryParams: any[];
|
|
13
|
+
pathParams: any[];
|
|
14
|
+
isVoidArg: boolean;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function generateTypesFile(
|
|
19
|
+
endpointInfos: EndpointInfo[],
|
|
20
|
+
_options: GenerationOptions,
|
|
21
|
+
schemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
|
|
22
|
+
operationDefinitions?: any[]
|
|
23
|
+
) {
|
|
24
|
+
|
|
25
|
+
// 生成 import 語句
|
|
26
|
+
let importStatement = `/* eslint-disable */
|
|
27
|
+
// [Warning] Generated automatically - do not edit manually
|
|
28
|
+
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
// 檢查是否需要引入 schema.ts
|
|
32
|
+
const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
|
|
33
|
+
if (hasSchemaTypes) {
|
|
34
|
+
importStatement += `import * as Schema from "../schema";\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
importStatement += '\n';
|
|
38
|
+
|
|
39
|
+
// 收集所有需要的類型定義
|
|
40
|
+
const typeDefinitions: string[] = [];
|
|
41
|
+
|
|
42
|
+
// 注意:不再在 types.ts 中重複生成 schema 類型
|
|
43
|
+
// schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
|
|
44
|
+
|
|
45
|
+
// 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
|
|
46
|
+
const endpointTypes: string[] = [];
|
|
47
|
+
|
|
48
|
+
// 為每個端點生成 Req/Res 類型
|
|
49
|
+
endpointInfos.forEach(endpoint => {
|
|
50
|
+
// 使用 endpoint 中提供的準確類型名稱
|
|
51
|
+
const reqTypeName = endpoint.argTypeName;
|
|
52
|
+
const resTypeName = endpoint.responseTypeName;
|
|
53
|
+
|
|
54
|
+
// 生成 Request 類型(總是生成)
|
|
55
|
+
if (reqTypeName) {
|
|
56
|
+
const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions);
|
|
57
|
+
if (requestTypeContent.trim() === '' || requestTypeContent.includes('TODO') || requestTypeContent.includes('[key: string]: any')) {
|
|
58
|
+
// 如果沒有實際內容,使用 void
|
|
59
|
+
endpointTypes.push(
|
|
60
|
+
`export type ${reqTypeName} = void;`,
|
|
61
|
+
``
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
// 有實際內容,使用 type 定義
|
|
65
|
+
endpointTypes.push(
|
|
66
|
+
`export type ${reqTypeName} = {`,
|
|
67
|
+
requestTypeContent,
|
|
68
|
+
`};`,
|
|
69
|
+
``
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 生成 Response 類型(總是生成)
|
|
75
|
+
if (resTypeName) {
|
|
76
|
+
const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions);
|
|
77
|
+
if (responseTypeContent.trim() === '' || responseTypeContent.includes('TODO') || responseTypeContent.includes('[key: string]: any')) {
|
|
78
|
+
// 如果沒有實際內容,使用 void
|
|
79
|
+
endpointTypes.push(
|
|
80
|
+
`export type ${resTypeName} = void;`,
|
|
81
|
+
``
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
// 有實際內容,使用 type 定義
|
|
85
|
+
endpointTypes.push(
|
|
86
|
+
`export type ${resTypeName} = {`,
|
|
87
|
+
responseTypeContent,
|
|
88
|
+
`};`,
|
|
89
|
+
``
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (endpointTypes.length > 0) {
|
|
96
|
+
typeDefinitions.push(endpointTypes.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 如果沒有任何類型定義,至少添加一些基本說明
|
|
100
|
+
if (typeDefinitions.length === 0) {
|
|
101
|
+
typeDefinitions.push(
|
|
102
|
+
`// 此檔案用於定義 API 相關的類型`,
|
|
103
|
+
`// 類型定義會根據 OpenAPI Schema 自動生成`,
|
|
104
|
+
``
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return importStatement + typeDefinitions.join('\n\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 生成 Request 類型的內容
|
|
113
|
+
*/
|
|
114
|
+
function generateRequestTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[]): string {
|
|
115
|
+
const properties: string[] = [];
|
|
116
|
+
|
|
117
|
+
// 如果有 query 參數
|
|
118
|
+
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
119
|
+
endpoint.queryParams.forEach(param => {
|
|
120
|
+
const optional = param.required ? '' : '?';
|
|
121
|
+
const paramType = getTypeFromParameter(param);
|
|
122
|
+
properties.push(` ${param.name}${optional}: ${paramType};`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 如果有 path 參數
|
|
127
|
+
if (endpoint.pathParams && endpoint.pathParams.length > 0) {
|
|
128
|
+
endpoint.pathParams.forEach(param => {
|
|
129
|
+
const optional = param.required ? '' : '?';
|
|
130
|
+
const paramType = getTypeFromParameter(param);
|
|
131
|
+
properties.push(` ${param.name}${optional}: ${paramType};`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 如果有 request body(從 operationDefinitions 中獲取)
|
|
136
|
+
const operationDef = operationDefinitions?.find(op => {
|
|
137
|
+
// 嘗試多種匹配方式
|
|
138
|
+
return op.operation?.operationId === endpoint.operationName ||
|
|
139
|
+
op.operation?.operationId === endpoint.operationName.toLowerCase() ||
|
|
140
|
+
// 也嘗試匹配 verb + path 組合
|
|
141
|
+
(op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (operationDef?.operation?.requestBody) {
|
|
145
|
+
const requestBody = operationDef.operation.requestBody;
|
|
146
|
+
const content = requestBody.content;
|
|
147
|
+
|
|
148
|
+
// 處理不同的 content types
|
|
149
|
+
const jsonContent = content['application/json'];
|
|
150
|
+
const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
|
|
151
|
+
|
|
152
|
+
if (jsonContent?.schema) {
|
|
153
|
+
const bodyType = getTypeFromSchema(jsonContent.schema);
|
|
154
|
+
properties.push(` body: ${bodyType};`);
|
|
155
|
+
} else if (formContent?.schema) {
|
|
156
|
+
const bodyType = getTypeFromSchema(formContent.schema);
|
|
157
|
+
properties.push(` body: ${bodyType};`);
|
|
158
|
+
} else {
|
|
159
|
+
properties.push(` body?: any; // Request body from OpenAPI`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 如果沒有任何參數,返回空內容(將由調用方處理為 void)
|
|
164
|
+
if (properties.length === 0) {
|
|
165
|
+
return ''; // 返回空字串,讓調用方決定使用 void
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return properties.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 生成 Response 類型的內容
|
|
173
|
+
*/
|
|
174
|
+
function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinitions?: any[]): string {
|
|
175
|
+
const properties: string[] = [];
|
|
176
|
+
|
|
177
|
+
// 嘗試從 operationDefinitions 中獲取響應結構
|
|
178
|
+
const operationDef = operationDefinitions?.find(op => {
|
|
179
|
+
// 嘗試多種匹配方式
|
|
180
|
+
return op.operation?.operationId === endpoint.operationName ||
|
|
181
|
+
op.operation?.operationId === endpoint.operationName.toLowerCase() ||
|
|
182
|
+
// 也嘗試匹配 verb + path 組合
|
|
183
|
+
(op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (operationDef?.operation?.responses) {
|
|
187
|
+
// 檢查 200 響應
|
|
188
|
+
const successResponse = operationDef.operation.responses['200'] ||
|
|
189
|
+
operationDef.operation.responses['201'];
|
|
190
|
+
|
|
191
|
+
if (successResponse?.content) {
|
|
192
|
+
const jsonContent = successResponse.content['application/json'];
|
|
193
|
+
if (jsonContent?.schema) {
|
|
194
|
+
const responseProps = parseSchemaProperties(jsonContent.schema);
|
|
195
|
+
properties.push(...responseProps);
|
|
196
|
+
} else {
|
|
197
|
+
properties.push(` // Success response from OpenAPI`);
|
|
198
|
+
properties.push(` data?: any;`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 如果沒有響應定義,返回空內容(將由調用方處理為 void)
|
|
204
|
+
if (properties.length === 0) {
|
|
205
|
+
return ''; // 返回空字串,讓調用方決定使用 void
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return properties.join('\n');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 解析 OpenAPI schema 的 properties 並生成 TypeScript 屬性定義
|
|
213
|
+
*/
|
|
214
|
+
function parseSchemaProperties(schema: any): string[] {
|
|
215
|
+
const properties: string[] = [];
|
|
216
|
+
|
|
217
|
+
if (schema.type === 'object' && schema.properties) {
|
|
218
|
+
const required = schema.required || [];
|
|
219
|
+
|
|
220
|
+
Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => {
|
|
221
|
+
const isRequired = required.includes(propName);
|
|
222
|
+
const optional = isRequired ? '' : '?';
|
|
223
|
+
const propType = getTypeFromSchema(propSchema);
|
|
224
|
+
const description = propSchema.description ? ` // ${propSchema.description}` : '';
|
|
225
|
+
|
|
226
|
+
properties.push(` ${propName}${optional}: ${propType};${description}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return properties;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 從 OpenAPI schema 獲取 TypeScript 類型
|
|
235
|
+
*/
|
|
236
|
+
function getTypeFromSchema(schema: any): string {
|
|
237
|
+
if (!schema) return 'any';
|
|
238
|
+
|
|
239
|
+
// 處理 $ref 引用,使用 Schema.TypeName 格式
|
|
240
|
+
if (schema.$ref) {
|
|
241
|
+
const refPath = schema.$ref;
|
|
242
|
+
if (refPath.startsWith('#/components/schemas/')) {
|
|
243
|
+
const typeName = refPath.replace('#/components/schemas/', '');
|
|
244
|
+
return `Schema.${typeName}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
switch (schema.type) {
|
|
249
|
+
case 'string':
|
|
250
|
+
if (schema.enum) {
|
|
251
|
+
return schema.enum.map((val: string) => `"${val}"`).join(' | ');
|
|
252
|
+
}
|
|
253
|
+
return 'string';
|
|
254
|
+
case 'number':
|
|
255
|
+
case 'integer':
|
|
256
|
+
return 'number';
|
|
257
|
+
case 'boolean':
|
|
258
|
+
return 'boolean';
|
|
259
|
+
case 'array':
|
|
260
|
+
const itemType = schema.items ? getTypeFromSchema(schema.items) : 'any';
|
|
261
|
+
return `${itemType}[]`;
|
|
262
|
+
case 'object':
|
|
263
|
+
if (schema.properties) {
|
|
264
|
+
// 如果有具體的屬性定義,生成內聯對象類型
|
|
265
|
+
const props = Object.entries(schema.properties).map(([key, propSchema]: [string, any]) => {
|
|
266
|
+
const required = schema.required || [];
|
|
267
|
+
const optional = required.includes(key) ? '' : '?';
|
|
268
|
+
const type = getTypeFromSchema(propSchema);
|
|
269
|
+
return `${key}${optional}: ${type}`;
|
|
270
|
+
}).join('; ');
|
|
271
|
+
return `{ ${props} }`;
|
|
272
|
+
}
|
|
273
|
+
return 'any';
|
|
274
|
+
default:
|
|
275
|
+
return 'any';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 從參數定義中獲取 TypeScript 類型
|
|
281
|
+
*/
|
|
282
|
+
function getTypeFromParameter(param: any): string {
|
|
283
|
+
if (!param.schema) return 'any';
|
|
284
|
+
return getTypeFromSchema(param.schema);
|
|
285
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 產生 Utils function 內容
|
|
6
|
+
* @param interfaces
|
|
7
|
+
*/
|
|
8
|
+
export function generateUtilsFile() {
|
|
9
|
+
|
|
10
|
+
// 分析接口內容以找出需要從 shared-types 導入的類型
|
|
11
|
+
return `/* eslint-disable */
|
|
12
|
+
// [Warning] Generated automatically - do not edit manually
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clear undefined in object
|
|
16
|
+
*/
|
|
17
|
+
export function withoutUndefined(obj?: Record<string, any>) {
|
|
18
|
+
if(typeof obj === 'undefined') return;
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.entries(obj).filter(([_, v]) => v !== undefined && v !== null)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 大寫底線字串 轉 小駝峰
|
|
29
|
+
* @param str
|
|
30
|
+
*/
|
|
31
|
+
export function toCamelCase(str: string): string {
|
|
32
|
+
return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { UnifiedCodeGenerator, type UnifiedGenerationOptions, type UnifiedGenerationResult } from './services/unified-code-generator';
|
|
2
|
+
|
|
3
|
+
export type { OutputFilesConfig, ConfigFile } from './types';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 產生 Endpoints - 直接使用統一代碼生成器
|
|
8
|
+
* @param options - 端點生成選項
|
|
9
|
+
*/
|
|
10
|
+
export async function generateEndpoints(options: UnifiedGenerationOptions): Promise<string | void> {
|
|
11
|
+
const generator = new UnifiedCodeGenerator(options);
|
|
12
|
+
|
|
13
|
+
const result = await generator.generateAll();
|
|
14
|
+
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
if (result.errors.length > 0) {
|
|
17
|
+
throw result.errors[0];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|