@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
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import type { GenerationOptions } from '../types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 轉換類型名稱為大駝峰命名
|
|
6
|
+
*/
|
|
7
|
+
const toPascalCase = (name: string): string => {
|
|
8
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
export interface EndpointInfo {
|
|
5
12
|
operationName: string;
|
|
6
13
|
argTypeName: string;
|
|
@@ -16,182 +23,208 @@ export interface EndpointInfo {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export function generateTypesFile(
|
|
19
|
-
endpointInfos: EndpointInfo[],
|
|
26
|
+
endpointInfos: EndpointInfo[],
|
|
20
27
|
_options: GenerationOptions,
|
|
21
28
|
schemaInterfaces?: Record<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>,
|
|
22
29
|
operationDefinitions?: any[]
|
|
23
30
|
) {
|
|
31
|
+
// 創建 schema 類型名稱映射表 - 使用實際生成的類型名稱
|
|
32
|
+
const schemaTypeMap: Record<string, string> = {};
|
|
33
|
+
|
|
34
|
+
if (schemaInterfaces) {
|
|
35
|
+
Object.keys(schemaInterfaces).forEach((actualTypeName) => {
|
|
36
|
+
// 直接使用實際生成的類型名稱,不進行轉換
|
|
37
|
+
// 這確保了 types.ts 中的引用與 schema.ts 中的定義完全一致
|
|
38
|
+
schemaTypeMap[actualTypeName] = actualTypeName;
|
|
39
|
+
|
|
40
|
+
// 為了兼容 OpenAPI 中可能存在的不同命名方式,添加常見的映射
|
|
41
|
+
// 例如:TokenResponseVO -> TokenResponseVo
|
|
42
|
+
if (actualTypeName.endsWith('Vo')) {
|
|
43
|
+
const openApiName = actualTypeName.slice(0, -2) + 'VO';
|
|
44
|
+
schemaTypeMap[openApiName] = actualTypeName;
|
|
45
|
+
}
|
|
46
|
+
if (actualTypeName.endsWith('Dto')) {
|
|
47
|
+
const openApiName = actualTypeName.slice(0, -3) + 'DTO';
|
|
48
|
+
schemaTypeMap[openApiName] = actualTypeName;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
24
52
|
|
|
25
53
|
// 生成 import 語句
|
|
26
54
|
let importStatement = `/* eslint-disable */
|
|
27
55
|
// [Warning] Generated automatically - do not edit manually
|
|
28
56
|
|
|
29
57
|
`;
|
|
30
|
-
|
|
58
|
+
|
|
31
59
|
// 檢查是否需要引入 schema.ts
|
|
32
60
|
const hasSchemaTypes = schemaInterfaces && Object.keys(schemaInterfaces).length > 0;
|
|
33
61
|
if (hasSchemaTypes) {
|
|
34
62
|
importStatement += `import * as Schema from "../schema";\n`;
|
|
35
63
|
}
|
|
36
|
-
|
|
64
|
+
|
|
37
65
|
importStatement += '\n';
|
|
38
|
-
|
|
66
|
+
|
|
39
67
|
// 收集所有需要的類型定義
|
|
40
68
|
const typeDefinitions: string[] = [];
|
|
41
|
-
|
|
69
|
+
|
|
42
70
|
// 注意:不再在 types.ts 中重複生成 schema 類型
|
|
43
71
|
// schema 類型已經在 schema.ts 中生成,這裡直接使用 Schema.* 引用
|
|
44
|
-
|
|
72
|
+
|
|
45
73
|
// 無論是否有 schema,都要生成 endpoint 特定的 Req/Res 類型
|
|
46
74
|
const endpointTypes: string[] = [];
|
|
47
|
-
|
|
75
|
+
|
|
48
76
|
// 為每個端點生成 Req/Res 類型
|
|
49
|
-
endpointInfos.forEach(endpoint => {
|
|
77
|
+
endpointInfos.forEach((endpoint) => {
|
|
50
78
|
// 使用 endpoint 中提供的準確類型名稱
|
|
51
79
|
const reqTypeName = endpoint.argTypeName;
|
|
52
80
|
const resTypeName = endpoint.responseTypeName;
|
|
53
|
-
|
|
81
|
+
|
|
54
82
|
// 生成 Request 類型(總是生成)
|
|
55
83
|
if (reqTypeName) {
|
|
56
|
-
const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions);
|
|
57
|
-
if (requestTypeContent.trim() === ''
|
|
84
|
+
const requestTypeContent = generateRequestTypeContent(endpoint, operationDefinitions, schemaTypeMap);
|
|
85
|
+
if (requestTypeContent.trim() === '') {
|
|
58
86
|
// 如果沒有實際內容,使用 void
|
|
59
|
-
endpointTypes.push(
|
|
60
|
-
`export type ${reqTypeName} = void;`,
|
|
61
|
-
``
|
|
62
|
-
);
|
|
87
|
+
endpointTypes.push(`export type ${reqTypeName} = void;`, ``);
|
|
63
88
|
} else {
|
|
64
89
|
// 有實際內容,使用 type 定義
|
|
65
|
-
endpointTypes.push(
|
|
66
|
-
`export type ${reqTypeName} = {`,
|
|
67
|
-
requestTypeContent,
|
|
68
|
-
`};`,
|
|
69
|
-
``
|
|
70
|
-
);
|
|
90
|
+
endpointTypes.push(`export type ${reqTypeName} = {`, requestTypeContent, `};`, ``);
|
|
71
91
|
}
|
|
72
92
|
}
|
|
73
|
-
|
|
93
|
+
|
|
74
94
|
// 生成 Response 類型(總是生成)
|
|
75
95
|
if (resTypeName) {
|
|
76
|
-
const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions);
|
|
77
|
-
if (responseTypeContent.trim() === ''
|
|
96
|
+
const responseTypeContent = generateResponseTypeContent(endpoint, operationDefinitions, schemaTypeMap);
|
|
97
|
+
if (responseTypeContent.trim() === '') {
|
|
78
98
|
// 如果沒有實際內容,使用 void
|
|
79
|
-
endpointTypes.push(
|
|
80
|
-
`export type ${resTypeName} = void;`,
|
|
81
|
-
``
|
|
82
|
-
);
|
|
99
|
+
endpointTypes.push(`export type ${resTypeName} = void;`, ``);
|
|
83
100
|
} else {
|
|
84
101
|
// 有實際內容,使用 type 定義
|
|
85
|
-
endpointTypes.push(
|
|
86
|
-
`export type ${resTypeName} = {`,
|
|
87
|
-
responseTypeContent,
|
|
88
|
-
`};`,
|
|
89
|
-
``
|
|
90
|
-
);
|
|
102
|
+
endpointTypes.push(`export type ${resTypeName} = {`, responseTypeContent, `};`, ``);
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
});
|
|
94
|
-
|
|
106
|
+
|
|
95
107
|
if (endpointTypes.length > 0) {
|
|
96
108
|
typeDefinitions.push(endpointTypes.join('\n'));
|
|
97
109
|
}
|
|
98
|
-
|
|
110
|
+
|
|
99
111
|
// 如果沒有任何類型定義,至少添加一些基本說明
|
|
100
112
|
if (typeDefinitions.length === 0) {
|
|
101
|
-
typeDefinitions.push(
|
|
102
|
-
`// 此檔案用於定義 API 相關的類型`,
|
|
103
|
-
`// 類型定義會根據 OpenAPI Schema 自動生成`,
|
|
104
|
-
``
|
|
105
|
-
);
|
|
113
|
+
typeDefinitions.push(`// 此檔案用於定義 API 相關的類型`, `// 類型定義會根據 OpenAPI Schema 自動生成`, ``);
|
|
106
114
|
}
|
|
107
|
-
|
|
115
|
+
|
|
108
116
|
return importStatement + typeDefinitions.join('\n\n');
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
/**
|
|
112
120
|
* 生成 Request 類型的內容
|
|
113
121
|
*/
|
|
114
|
-
function generateRequestTypeContent(
|
|
122
|
+
function generateRequestTypeContent(
|
|
123
|
+
endpoint: EndpointInfo,
|
|
124
|
+
operationDefinitions?: any[],
|
|
125
|
+
schemaTypeMap: Record<string, string> = {}
|
|
126
|
+
): string {
|
|
115
127
|
const properties: string[] = [];
|
|
116
|
-
|
|
128
|
+
|
|
117
129
|
// 如果有 query 參數
|
|
118
130
|
if (endpoint.queryParams && endpoint.queryParams.length > 0) {
|
|
119
|
-
endpoint.queryParams.forEach(param => {
|
|
131
|
+
endpoint.queryParams.forEach((param) => {
|
|
120
132
|
const optional = param.required ? '' : '?';
|
|
121
|
-
const paramType = getTypeFromParameter(param);
|
|
133
|
+
const paramType = getTypeFromParameter(param, schemaTypeMap);
|
|
122
134
|
properties.push(` ${param.name}${optional}: ${paramType};`);
|
|
123
135
|
});
|
|
124
136
|
}
|
|
125
|
-
|
|
137
|
+
|
|
126
138
|
// 如果有 path 參數
|
|
127
139
|
if (endpoint.pathParams && endpoint.pathParams.length > 0) {
|
|
128
|
-
endpoint.pathParams.forEach(param => {
|
|
140
|
+
endpoint.pathParams.forEach((param) => {
|
|
129
141
|
const optional = param.required ? '' : '?';
|
|
130
|
-
const paramType = getTypeFromParameter(param);
|
|
142
|
+
const paramType = getTypeFromParameter(param, schemaTypeMap);
|
|
131
143
|
properties.push(` ${param.name}${optional}: ${paramType};`);
|
|
132
144
|
});
|
|
133
145
|
}
|
|
134
|
-
|
|
146
|
+
|
|
135
147
|
// 如果有 request body(從 operationDefinitions 中獲取)
|
|
136
|
-
const operationDef = operationDefinitions?.find(op => {
|
|
148
|
+
const operationDef = operationDefinitions?.find((op) => {
|
|
137
149
|
// 嘗試多種匹配方式
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
return (
|
|
151
|
+
op.operation?.operationId === endpoint.operationName ||
|
|
152
|
+
op.operation?.operationId === endpoint.operationName.toLowerCase() ||
|
|
153
|
+
// 也嘗試匹配 verb + path 組合
|
|
154
|
+
(op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path)
|
|
155
|
+
);
|
|
142
156
|
});
|
|
143
|
-
|
|
157
|
+
|
|
144
158
|
if (operationDef?.operation?.requestBody) {
|
|
145
159
|
const requestBody = operationDef.operation.requestBody;
|
|
146
160
|
const content = requestBody.content;
|
|
147
|
-
|
|
148
|
-
// 處理不同的 content types
|
|
149
|
-
const jsonContent = content['application/json'];
|
|
161
|
+
|
|
162
|
+
// 處理不同的 content types,優先使用 application/json,其次嘗試其他類型
|
|
163
|
+
const jsonContent = content['application/json'] || content['*/*'];
|
|
150
164
|
const formContent = content['multipart/form-data'] || content['application/x-www-form-urlencoded'];
|
|
151
|
-
|
|
165
|
+
|
|
152
166
|
if (jsonContent?.schema) {
|
|
153
|
-
|
|
167
|
+
// indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
|
|
168
|
+
const bodyType = getTypeFromSchema(jsonContent.schema, schemaTypeMap, 1);
|
|
154
169
|
properties.push(` body: ${bodyType};`);
|
|
155
170
|
} else if (formContent?.schema) {
|
|
156
|
-
|
|
171
|
+
// indentLevel=1 因為 body 屬性已經在類型定義內(有 2 個空格縮排)
|
|
172
|
+
const bodyType = getTypeFromSchema(formContent.schema, schemaTypeMap, 1);
|
|
157
173
|
properties.push(` body: ${bodyType};`);
|
|
158
174
|
} else {
|
|
159
|
-
|
|
175
|
+
// fallback 到第一個可用的 content-type
|
|
176
|
+
const firstContent = Object.values(content)[0] as any;
|
|
177
|
+
if (firstContent?.schema) {
|
|
178
|
+
const bodyType = getTypeFromSchema(firstContent.schema, schemaTypeMap, 1);
|
|
179
|
+
properties.push(` body: ${bodyType};`);
|
|
180
|
+
} else {
|
|
181
|
+
properties.push(` body?: any; // Request body from OpenAPI`);
|
|
182
|
+
}
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
|
-
|
|
185
|
+
|
|
163
186
|
// 如果沒有任何參數,返回空內容(將由調用方處理為 void)
|
|
164
187
|
if (properties.length === 0) {
|
|
165
188
|
return ''; // 返回空字串,讓調用方決定使用 void
|
|
166
189
|
}
|
|
167
|
-
|
|
190
|
+
|
|
168
191
|
return properties.join('\n');
|
|
169
192
|
}
|
|
170
193
|
|
|
171
194
|
/**
|
|
172
195
|
* 生成 Response 類型的內容
|
|
173
196
|
*/
|
|
174
|
-
function generateResponseTypeContent(
|
|
197
|
+
function generateResponseTypeContent(
|
|
198
|
+
endpoint: EndpointInfo,
|
|
199
|
+
operationDefinitions?: any[],
|
|
200
|
+
schemaTypeMap: Record<string, string> = {}
|
|
201
|
+
): string {
|
|
175
202
|
const properties: string[] = [];
|
|
176
|
-
|
|
203
|
+
|
|
177
204
|
// 嘗試從 operationDefinitions 中獲取響應結構
|
|
178
|
-
const operationDef = operationDefinitions?.find(op => {
|
|
205
|
+
const operationDef = operationDefinitions?.find((op) => {
|
|
179
206
|
// 嘗試多種匹配方式
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
return (
|
|
208
|
+
op.operation?.operationId === endpoint.operationName ||
|
|
209
|
+
op.operation?.operationId === endpoint.operationName.toLowerCase() ||
|
|
210
|
+
// 也嘗試匹配 verb + path 組合
|
|
211
|
+
(op.verb === endpoint.verb.toLowerCase() && op.path === endpoint.path)
|
|
212
|
+
);
|
|
184
213
|
});
|
|
185
|
-
|
|
214
|
+
|
|
186
215
|
if (operationDef?.operation?.responses) {
|
|
187
216
|
// 檢查 200 響應
|
|
188
|
-
const successResponse = operationDef.operation.responses['200'] ||
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
const successResponse = operationDef.operation.responses['200'] || operationDef.operation.responses['201'];
|
|
218
|
+
|
|
191
219
|
if (successResponse?.content) {
|
|
192
|
-
|
|
220
|
+
// 優先使用 application/json,其次嘗試其他 content-type(包括 */*)
|
|
221
|
+
const jsonContent =
|
|
222
|
+
successResponse.content['application/json'] ||
|
|
223
|
+
successResponse.content['*/*'] ||
|
|
224
|
+
Object.values(successResponse.content)[0]; // fallback 到第一個可用的 content-type
|
|
225
|
+
|
|
193
226
|
if (jsonContent?.schema) {
|
|
194
|
-
const responseProps = parseSchemaProperties(jsonContent.schema);
|
|
227
|
+
const responseProps = parseSchemaProperties(jsonContent.schema, schemaTypeMap);
|
|
195
228
|
properties.push(...responseProps);
|
|
196
229
|
} else {
|
|
197
230
|
properties.push(` // Success response from OpenAPI`);
|
|
@@ -199,87 +232,158 @@ function generateResponseTypeContent(endpoint: EndpointInfo, operationDefinition
|
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
|
-
|
|
235
|
+
|
|
203
236
|
// 如果沒有響應定義,返回空內容(將由調用方處理為 void)
|
|
204
237
|
if (properties.length === 0) {
|
|
205
238
|
return ''; // 返回空字串,讓調用方決定使用 void
|
|
206
239
|
}
|
|
207
|
-
|
|
240
|
+
|
|
208
241
|
return properties.join('\n');
|
|
209
242
|
}
|
|
210
243
|
|
|
211
244
|
/**
|
|
212
245
|
* 解析 OpenAPI schema 的 properties 並生成 TypeScript 屬性定義
|
|
213
246
|
*/
|
|
214
|
-
function parseSchemaProperties(schema: any): string[] {
|
|
247
|
+
function parseSchemaProperties(schema: any, schemaTypeMap: Record<string, string> = {}): string[] {
|
|
215
248
|
const properties: string[] = [];
|
|
216
|
-
|
|
249
|
+
|
|
217
250
|
if (schema.type === 'object' && schema.properties) {
|
|
218
251
|
const required = schema.required || [];
|
|
219
|
-
|
|
252
|
+
|
|
220
253
|
Object.entries(schema.properties).forEach(([propName, propSchema]: [string, any]) => {
|
|
221
254
|
const isRequired = required.includes(propName);
|
|
222
255
|
const optional = isRequired ? '' : '?';
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
// indentLevel=1 因為屬性已經在類型定義內(有 2 個空格縮排)
|
|
257
|
+
const propType = getTypeFromSchema(propSchema, schemaTypeMap, 1);
|
|
258
|
+
|
|
259
|
+
// 如果屬性名包含特殊字符(如 -),需要加上引號
|
|
260
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(propName);
|
|
261
|
+
const quotedPropName = needsQuotes ? `"${propName}"` : propName;
|
|
262
|
+
|
|
263
|
+
// 生成 JSDoc 註解
|
|
264
|
+
if (propSchema.description) {
|
|
265
|
+
properties.push(` /** ${propSchema.description} */`);
|
|
266
|
+
}
|
|
267
|
+
properties.push(` ${quotedPropName}${optional}: ${propType};`);
|
|
227
268
|
});
|
|
228
269
|
}
|
|
229
|
-
|
|
270
|
+
|
|
230
271
|
return properties;
|
|
231
272
|
}
|
|
232
273
|
|
|
233
274
|
/**
|
|
234
275
|
* 從 OpenAPI schema 獲取 TypeScript 類型
|
|
276
|
+
* @param schema OpenAPI schema 定義
|
|
277
|
+
* @param schemaTypeMap 類型名稱映射表
|
|
278
|
+
* @param indentLevel 縮排層級,用於格式化內嵌物件
|
|
235
279
|
*/
|
|
236
|
-
function getTypeFromSchema(schema: any): string {
|
|
280
|
+
function getTypeFromSchema(schema: any, schemaTypeMap: Record<string, string> = {}, indentLevel: number = 0): string {
|
|
237
281
|
if (!schema) return 'any';
|
|
238
|
-
|
|
282
|
+
|
|
239
283
|
// 處理 $ref 引用,使用 Schema.TypeName 格式
|
|
240
284
|
if (schema.$ref) {
|
|
241
285
|
const refPath = schema.$ref;
|
|
242
286
|
if (refPath.startsWith('#/components/schemas/')) {
|
|
243
|
-
const
|
|
244
|
-
|
|
287
|
+
const originalTypeName = refPath.replace('#/components/schemas/', '');
|
|
288
|
+
// 使用映射表查找實際的類型名稱,並轉換為大駝峰
|
|
289
|
+
const actualTypeName = schemaTypeMap[originalTypeName] || originalTypeName;
|
|
290
|
+
const pascalCaseTypeName = toPascalCase(actualTypeName);
|
|
291
|
+
const baseType = `Schema.${pascalCaseTypeName}`;
|
|
292
|
+
// 處理 nullable
|
|
293
|
+
return schema.nullable ? `${baseType} | null` : baseType;
|
|
245
294
|
}
|
|
246
295
|
}
|
|
247
|
-
|
|
296
|
+
|
|
297
|
+
let baseType: string;
|
|
298
|
+
|
|
248
299
|
switch (schema.type) {
|
|
249
300
|
case 'string':
|
|
250
301
|
if (schema.enum) {
|
|
251
|
-
|
|
302
|
+
baseType = schema.enum.map((val: string) => `"${val}"`).join(' | ');
|
|
303
|
+
} else if (schema.format === 'binary') {
|
|
304
|
+
// 處理檔案上傳:format: "binary" 應該對應 Blob 類型
|
|
305
|
+
baseType = 'Blob';
|
|
306
|
+
} else {
|
|
307
|
+
baseType = 'string';
|
|
252
308
|
}
|
|
253
|
-
|
|
309
|
+
break;
|
|
254
310
|
case 'number':
|
|
255
311
|
case 'integer':
|
|
256
|
-
|
|
312
|
+
baseType = 'number';
|
|
313
|
+
break;
|
|
257
314
|
case 'boolean':
|
|
258
|
-
|
|
315
|
+
baseType = 'boolean';
|
|
316
|
+
break;
|
|
259
317
|
case 'array':
|
|
260
|
-
const itemType = schema.items ? getTypeFromSchema(schema.items) : 'any';
|
|
261
|
-
|
|
318
|
+
const itemType = schema.items ? getTypeFromSchema(schema.items, schemaTypeMap, indentLevel) : 'any';
|
|
319
|
+
// 如果 itemType 包含聯合類型(包含 |),需要加括號
|
|
320
|
+
const needsParentheses = itemType.includes('|');
|
|
321
|
+
baseType = needsParentheses ? `(${itemType})[]` : `${itemType}[]`;
|
|
322
|
+
break;
|
|
262
323
|
case 'object':
|
|
263
324
|
if (schema.properties) {
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
325
|
+
// 如果有具體的屬性定義,生成內聯對象類型(多行格式)
|
|
326
|
+
const entries = Object.entries(schema.properties);
|
|
327
|
+
|
|
328
|
+
// 如果沒有屬性但有 additionalProperties,生成 Record 類型
|
|
329
|
+
if (entries.length === 0) {
|
|
330
|
+
if (schema.additionalProperties) {
|
|
331
|
+
const valueType =
|
|
332
|
+
schema.additionalProperties === true
|
|
333
|
+
? 'any'
|
|
334
|
+
: getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
|
|
335
|
+
baseType = `Record<string, ${valueType}>`;
|
|
336
|
+
} else {
|
|
337
|
+
baseType = '{}';
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// 計算下一層的縮排
|
|
341
|
+
const nextIndent = ' '.repeat(indentLevel + 1);
|
|
342
|
+
const currentIndent = ' '.repeat(indentLevel);
|
|
343
|
+
|
|
344
|
+
const props: string[] = [];
|
|
345
|
+
entries.forEach(([key, propSchema]: [string, any]) => {
|
|
346
|
+
const required = schema.required || [];
|
|
347
|
+
const optional = required.includes(key) ? '' : '?';
|
|
348
|
+
const type = getTypeFromSchema(propSchema, schemaTypeMap, indentLevel + 1);
|
|
349
|
+
|
|
350
|
+
// 如果屬性名包含特殊字符(如 -),需要加上引號
|
|
351
|
+
const needsQuotes = /[^a-zA-Z0-9_$]/.test(key);
|
|
352
|
+
const quotedKey = needsQuotes ? `"${key}"` : key;
|
|
353
|
+
|
|
354
|
+
// 生成 JSDoc 註解
|
|
355
|
+
if (propSchema.description) {
|
|
356
|
+
props.push(`${nextIndent}/** ${propSchema.description} */`);
|
|
357
|
+
}
|
|
358
|
+
props.push(`${nextIndent}${quotedKey}${optional}: ${type};`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
baseType = `{\n${props.join('\n')}\n${currentIndent}}`;
|
|
362
|
+
}
|
|
363
|
+
} else if (schema.additionalProperties) {
|
|
364
|
+
// 如果沒有 properties 但有 additionalProperties
|
|
365
|
+
const valueType =
|
|
366
|
+
schema.additionalProperties === true
|
|
367
|
+
? 'any'
|
|
368
|
+
: getTypeFromSchema(schema.additionalProperties, schemaTypeMap, indentLevel);
|
|
369
|
+
baseType = `Record<string, ${valueType}>`;
|
|
370
|
+
} else {
|
|
371
|
+
baseType = 'any';
|
|
272
372
|
}
|
|
273
|
-
|
|
373
|
+
break;
|
|
274
374
|
default:
|
|
275
|
-
|
|
375
|
+
baseType = 'any';
|
|
376
|
+
break;
|
|
276
377
|
}
|
|
378
|
+
|
|
379
|
+
// 處理 nullable
|
|
380
|
+
return schema.nullable ? `${baseType} | null` : baseType;
|
|
277
381
|
}
|
|
278
382
|
|
|
279
383
|
/**
|
|
280
384
|
* 從參數定義中獲取 TypeScript 類型
|
|
281
385
|
*/
|
|
282
|
-
function getTypeFromParameter(param: any): string {
|
|
386
|
+
function getTypeFromParameter(param: any, schemaTypeMap: Record<string, string> = {}): string {
|
|
283
387
|
if (!param.schema) return 'any';
|
|
284
|
-
return getTypeFromSchema(param.schema);
|
|
285
|
-
}
|
|
388
|
+
return getTypeFromSchema(param.schema, schemaTypeMap);
|
|
389
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* 產生 Utils function 內容
|
|
6
5
|
* @param interfaces
|
|
7
6
|
*/
|
|
8
7
|
export function generateUtilsFile() {
|
|
9
|
-
|
|
10
8
|
// 分析接口內容以找出需要從 shared-types 導入的類型
|
|
11
9
|
return `/* eslint-disable */
|
|
12
10
|
// [Warning] Generated automatically - do not edit manually
|
|
@@ -21,7 +19,7 @@ export function withoutUndefined(obj?: Record<string, any>) {
|
|
|
21
19
|
);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
`;
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
/**
|
|
@@ -30,4 +28,4 @@ export function withoutUndefined(obj?: Record<string, any>) {
|
|
|
30
28
|
*/
|
|
31
29
|
export function toCamelCase(str: string): string {
|
|
32
30
|
return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
33
|
-
}
|
|
31
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
UnifiedCodeGenerator,
|
|
3
|
+
type UnifiedGenerationOptions,
|
|
4
|
+
type UnifiedGenerationResult,
|
|
5
|
+
} from './services/unified-code-generator';
|
|
2
6
|
|
|
3
7
|
export type { OutputFilesConfig, ConfigFile } from './types';
|
|
4
8
|
|
|
5
|
-
|
|
6
9
|
/**
|
|
7
10
|
* 產生 Endpoints - 直接使用統一代碼生成器
|
|
8
11
|
* @param options - 端點生成選項
|
|
@@ -11,7 +14,7 @@ export async function generateEndpoints(options: UnifiedGenerationOptions): Prom
|
|
|
11
14
|
const generator = new UnifiedCodeGenerator(options);
|
|
12
15
|
|
|
13
16
|
const result = await generator.generateAll();
|
|
14
|
-
|
|
17
|
+
|
|
15
18
|
if (!result.success) {
|
|
16
19
|
if (result.errors.length > 0) {
|
|
17
20
|
throw result.errors[0];
|