swgto-ts 3.0.1 → 3.1.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 CHANGED
@@ -81,6 +81,7 @@ export interface SwaggerTsConfig {
81
81
  fileNaming?: 'module' | 'path'
82
82
  flattenQueryParam?: boolean
83
83
  mergeParams?: boolean
84
+ flattenOnGet?: boolean
84
85
  apiDocs?: ApiDocsConfig
85
86
  }
86
87
  ```
@@ -102,6 +103,7 @@ export interface SwaggerTsConfig {
102
103
  | `cleanOutput` | `boolean` | `false` | 生成前是否清空输出目录 |
103
104
  | `flattenQueryParam` | `boolean` | `false` | 当 query 参数只有一个且为 `$ref` 引用类型时,直接用引用类型代替 `{ query: RefType }` |
104
105
  | `mergeParams` | `boolean` | `false` | 合并参数层级:`true` 时展平 `path/query/body` 嵌套,直接使用 `params: { field1, field2 }` 代替 `params: { path: {...}, query: {...} }` |
106
+ | `flattenOnGet` | `boolean` | `false` | GET 请求专用展平:将所有 body/query/path 中的 `$ref` 参数展开为顶层交叉类型,彻底消除嵌套。详见 [GET 参数展平](#get-参数展平) |
105
107
  | `apiDocs` | `ApiDocsConfig` | 见说明 | 自动生成 API 文档(HTML/Markdown),详细配置见 [API 文档生成](#api-文档生成) |
106
108
 
107
109
  ### 函数名生成规则
@@ -148,6 +150,26 @@ getUser({ path: { id: '1' }, query: { name: 'foo' } })
148
150
  getUser({ id: '1', name: 'foo' })
149
151
  ```
150
152
 
153
+ ### GET 参数展平
154
+
155
+ `flattenOnGet: true` 专门解决 GET 请求中 body 和 query 参数嵌套的问题。当 GET 请求同时包含 body 参数(如 `userDTO`)和 query 参数(如 `pageQuery`)时,会将所有 `$ref` 类型的参数展开为顶层交叉类型,实现 `{...userDTO, ...pageQuery}` 的效果。
156
+
157
+ **背景**:部分后端定义 GET 请求时,参数在 body 和 query 中分散定义,但实际入参是合并平铺的。仅靠 `mergeParams` 会保留 body 内部的属性名作为嵌套字段。
158
+
159
+ **示例**:一个 GET 请求的 body 中有 `adminUserVO: AdminUserVO`,query 中有 `pageRequest: SessionPageRequestDTO`
160
+
161
+ ```ts
162
+ // flattenOnGet: false(默认),body 属性名保留为嵌套字段
163
+ get_ai_sessions({ adminUserVO: {...}, pageRequest: {...} })
164
+ // 类型:{ adminUserVO: AdminUserVO; pageRequest: SessionPageRequestDTO; }
165
+
166
+ // flattenOnGet: true,$ref 参数全部展开到顶层
167
+ get_ai_sessions({ ...adminUserVOFields, ...pageRequestFields })
168
+ // 类型:AdminUserVO & SessionPageRequestDTO
169
+ ```
170
+
171
+ > **注意**:`flattenOnGet` 仅对 GET 请求生效,且通常与 `mergeParams` 和 `flattenQueryParam` 一起使用效果最佳。该选项会同时影响生成的类型签名和函数体实现。
172
+
151
173
  ### API 文档生成
152
174
 
153
175
  配置 `apiDocs` 可在生成代码的同时,自动输出一份人类可读的 API 文档:
@@ -52,6 +52,7 @@ async function loadConfig(cwd) {
52
52
  fileNaming: rawConfig.fileNaming ?? "path",
53
53
  flattenQueryParam: rawConfig.flattenQueryParam ?? false,
54
54
  mergeParams: rawConfig.mergeParams ?? false,
55
+ flattenOnGet: rawConfig.flattenOnGet ?? false,
55
56
  apiDocs: {
56
57
  enable: rawConfig.apiDocs?.enable ?? false,
57
58
  output: rawConfig.apiDocs?.output ?? defaultOutput,
@@ -259,28 +260,60 @@ function parsePaths(document, docUrl, config) {
259
260
  const pathFields = pathParams.map((param) => `${param.name}: ${schemaToTs(param.schema)};`);
260
261
  const queryFields = queryParams.map((param) => `${param.name}${param.required ? "" : "?"}: ${schemaToTs(param.schema)};`);
261
262
  const requestTypeParts = [];
262
- if (bodyType) {
263
- requestTypeParts.push(bodyType);
264
- }
265
- if (pathFields.length) {
266
- if (config.mergeParams) {
267
- requestTypeParts.push(`{ ${pathFields.join(" ")} }`);
268
- } else {
269
- requestTypeParts.push(`{ path: { ${pathFields.join(" ")} } }`);
263
+ const isFlattenOnGet = config.flattenOnGet && method === "get";
264
+ if (isFlattenOnGet) {
265
+ const flatFields = [];
266
+ if (requestBodySchema) {
267
+ if (!requestBodySchema.$ref && requestBodySchema.properties) {
268
+ for (const [name, schema] of Object.entries(requestBodySchema.properties)) {
269
+ if (schema.$ref) {
270
+ requestTypeParts.push(schemaToTs(schema));
271
+ } else {
272
+ const required = requestBodySchema.required?.includes(name) ?? false;
273
+ flatFields.push(`${name}${required ? "" : "?"}: ${schemaToTs(schema)}`);
274
+ }
275
+ }
276
+ } else {
277
+ requestTypeParts.push(bodyType);
278
+ }
270
279
  }
271
- }
272
- if (queryFields.length) {
273
- if (config.flattenQueryParam && queryParams.length === 1 && queryParams[0].schema?.$ref) {
274
- if (config.mergeParams) {
275
- requestTypeParts.push(schemaToTs(queryParams[0].schema));
280
+ for (const param of queryParams) {
281
+ if (param.schema?.$ref) {
282
+ requestTypeParts.push(schemaToTs(param.schema));
276
283
  } else {
277
- requestTypeParts.push(`{ query: ${schemaToTs(queryParams[0].schema)} }`);
284
+ flatFields.push(`${param.name}${param.required ? "" : "?"}: ${schemaToTs(param.schema)}`);
278
285
  }
279
- } else {
286
+ }
287
+ for (const field of pathFields) {
288
+ flatFields.push(field);
289
+ }
290
+ if (flatFields.length) {
291
+ requestTypeParts.push(`{ ${flatFields.join(" ")} }`);
292
+ }
293
+ } else {
294
+ if (bodyType) {
295
+ requestTypeParts.push(bodyType);
296
+ }
297
+ if (pathFields.length) {
280
298
  if (config.mergeParams) {
281
- requestTypeParts.push(`{ ${queryFields.join(" ")} }`);
299
+ requestTypeParts.push(`{ ${pathFields.join(" ")} }`);
282
300
  } else {
283
- requestTypeParts.push(`{ query: { ${queryFields.join(" ")} } }`);
301
+ requestTypeParts.push(`{ path: { ${pathFields.join(" ")} } }`);
302
+ }
303
+ }
304
+ if (queryFields.length) {
305
+ if (config.flattenQueryParam && queryParams.length === 1 && queryParams[0].schema?.$ref) {
306
+ if (config.mergeParams) {
307
+ requestTypeParts.push(schemaToTs(queryParams[0].schema));
308
+ } else {
309
+ requestTypeParts.push(`{ query: ${schemaToTs(queryParams[0].schema)} }`);
310
+ }
311
+ } else {
312
+ if (config.mergeParams) {
313
+ requestTypeParts.push(`{ ${queryFields.join(" ")} }`);
314
+ } else {
315
+ requestTypeParts.push(`{ query: { ${queryFields.join(" ")} } }`);
316
+ }
284
317
  }
285
318
  }
286
319
  }
@@ -338,6 +371,16 @@ ${[...exportSet].join("\n")}
338
371
  }
339
372
 
340
373
  // src/generators/genRequestJs.ts
374
+ function isSimpleTypeRef(name) {
375
+ return /^[\w.]+$/.test(name);
376
+ }
377
+ function buildParamTag(requestParamType, typeImportPath) {
378
+ if (requestParamType === "void") return "[params]";
379
+ if (isSimpleTypeRef(requestParamType)) {
380
+ return `{import(${JSON.stringify(typeImportPath)}).${requestParamType}} params`;
381
+ }
382
+ return `{${requestParamType}} params`;
383
+ }
341
384
  function buildFunctionDoc(operation, typeImportPath, requestParamType) {
342
385
  const lines = [];
343
386
  if (operation.summary) {
@@ -349,7 +392,7 @@ function buildFunctionDoc(operation, typeImportPath, requestParamType) {
349
392
  lines.push(`@path ${operation.path}`);
350
393
  lines.push(`@requestPath ${operation.requestPath}`);
351
394
  lines.push(`@method ${operation.method.toUpperCase()}`);
352
- lines.push(`@param ${requestParamType === "void" ? "[params]" : `{import(${JSON.stringify(typeImportPath)}).${requestParamType}} params`}`);
395
+ lines.push(`@param ${buildParamTag(requestParamType, typeImportPath)}`);
353
396
  lines.push("@param [config]");
354
397
  if (operation.responseTypeName) {
355
398
  lines.push(`@returns {Promise<import(${JSON.stringify(typeImportPath)}).${operation.responseTypeName}>}`);
@@ -359,31 +402,43 @@ function buildFunctionDoc(operation, typeImportPath, requestParamType) {
359
402
  }
360
403
  return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
361
404
  }
362
- function renderFunction(operation, useGenericUrl, mergeParams) {
363
- const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
405
+ function renderFunction(operation, useGenericUrl, mergeParams, flattenOnGet = false) {
406
+ const effectiveMerge = mergeParams || flattenOnGet && operation.method === "get";
407
+ const queryLine = operation.queryParams.length ? ` params: ${effectiveMerge ? "params" : "params?.query"},
364
408
  ` : "";
365
409
  const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
366
- const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
410
+ const hasBodySchema = Boolean(operation.requestBodySchema);
411
+ const needsCleanData = effectiveMerge && hasBodySchema && operation.pathParams.length > 0;
412
+ const destructureLine = needsCleanData ? ` const { ${operation.pathParams.map((p) => p.name).join(", ")}, ...requestData } = params;
413
+ ` : "";
414
+ const dataValue = needsCleanData ? "requestData" : effectiveMerge ? "params" : bodyOnly ? "params" : "params?.body";
415
+ const bodyLine = hasBodySchema ? ` data: ${dataValue},
367
416
  ` : "";
368
- const pathArg = mergeParams ? "params" : "params?.path";
417
+ const pathArg = effectiveMerge ? "params" : "params?.path";
369
418
  const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
370
419
  const pathLine = ` url: ${urlValue},
371
420
  `;
372
421
  return `export async function ${operation.functionName}(params, config) {
373
- return request({
422
+ ${destructureLine} return request({
374
423
  ${pathLine} method: ${JSON.stringify(operation.method)},
375
424
  ${queryLine}${bodyLine} ...config,
376
425
  });
377
426
  }`;
378
427
  }
379
- function generateJsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
428
+ function generateJsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false, flattenOnGet = false) {
429
+ const effectiveMerge = mergeParams || flattenOnGet && operation.method === "get";
380
430
  const requestParamType = operation.requestTypeExpression ?? "void";
381
- const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
431
+ const queryLine = operation.queryParams.length ? ` params: ${effectiveMerge ? "params" : "params?.query"},
382
432
  ` : "";
383
433
  const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
384
- const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
434
+ const hasBodySchema = Boolean(operation.requestBodySchema);
435
+ const needsCleanData = effectiveMerge && hasBodySchema && operation.pathParams.length > 0;
436
+ const destructureLine = needsCleanData ? ` const { ${operation.pathParams.map((p) => p.name).join(", ")}, ...requestData } = params;
385
437
  ` : "";
386
- const pathArg = mergeParams ? "params" : "params?.path";
438
+ const dataValue = needsCleanData ? "requestData" : effectiveMerge ? "params" : bodyOnly ? "params" : "params?.body";
439
+ const bodyLine = hasBodySchema ? ` data: ${dataValue},
440
+ ` : "";
441
+ const pathArg = effectiveMerge ? "params" : "params?.path";
387
442
  const pathLine = operation.pathParams.length ? ` url: buildUrl(${pathArg}),
388
443
  ` : ` url: ${JSON.stringify(operation.requestPath)},
389
444
  `;
@@ -399,14 +454,14 @@ ${buildUrlHelper}
399
454
 
400
455
  ${buildFunctionDoc(operation, typeImportPath, requestParamType)}
401
456
  export async function ${operation.functionName}(params, config) {
402
- return request({
457
+ ${destructureLine} return request({
403
458
  ${pathLine} method: ${JSON.stringify(operation.method)},
404
459
  ${queryLine}${bodyLine} ...config,
405
460
  });
406
461
  }
407
462
  `;
408
463
  }
409
- function generateJsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
464
+ function generateJsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false, flattenOnGet = false) {
410
465
  const needsBuildUrl = operations.some((op) => op.pathParams.length > 0);
411
466
  const buildUrlHelper = needsBuildUrl ? `
412
467
  function buildUrl(url, path) {
@@ -417,7 +472,7 @@ function buildUrl(url, path) {
417
472
  const doc = buildFunctionDoc(op, typeImportPath, op.requestTypeExpression ?? "void");
418
473
  return `${doc}
419
474
 
420
- ${renderFunction(op, true, mergeParams)}`;
475
+ ${renderFunction(op, true, mergeParams, flattenOnGet)}`;
421
476
  }).join("\n\n");
422
477
  return `/* eslint-disable */
423
478
  // Auto-generated by swgto.
@@ -441,26 +496,32 @@ function buildFunctionDoc2(operation) {
441
496
  }
442
497
  return ["/**", ...lines.map((line) => ` * ${line}`), " */", ""].join("\n");
443
498
  }
444
- function renderFunction2(operation, useGenericUrl, mergeParams) {
499
+ function renderFunction2(operation, useGenericUrl, mergeParams, flattenOnGet = false) {
500
+ const effectiveMerge = mergeParams || flattenOnGet && operation.method === "get";
445
501
  const requestArg = operation.requestTypeExpression ? `params: ${operation.requestTypeExpression}, config?: RequestConfig` : "params?: void, config?: RequestConfig";
446
502
  const defaultResponseType = operation.responseTypeName ?? "unknown";
447
503
  const bodyOnly = Boolean(operation.requestBodySchema) && !operation.queryParams.length && !operation.pathParams.length;
448
- const bodyLine = operation.requestBodySchema ? ` data: ${mergeParams ? "params" : bodyOnly ? "params" : "params?.body"},
504
+ const hasBodySchema = Boolean(operation.requestBodySchema);
505
+ const needsCleanData = effectiveMerge && hasBodySchema && operation.pathParams.length > 0;
506
+ const destructureLine = needsCleanData ? ` const { ${operation.pathParams.map((p) => p.name).join(", ")}, ...requestData } = params;
507
+ ` : "";
508
+ const dataValue = needsCleanData ? "requestData" : effectiveMerge ? "params" : bodyOnly ? "params" : "params?.body";
509
+ const bodyLine = hasBodySchema ? ` data: ${dataValue},
449
510
  ` : "";
450
- const queryLine = operation.queryParams.length ? ` params: ${mergeParams ? "params" : "params?.query"},
511
+ const queryLine = operation.queryParams.length ? ` params: ${effectiveMerge ? "params" : "params?.query"},
451
512
  ` : "";
452
- const pathArg = mergeParams ? "params" : "params?.path";
513
+ const pathArg = effectiveMerge ? "params" : "params?.path";
453
514
  const urlValue = useGenericUrl && operation.pathParams.length ? `buildUrl(${JSON.stringify(operation.requestPath)}, ${pathArg})` : operation.pathParams.length ? `buildUrl(${pathArg})` : JSON.stringify(operation.requestPath);
454
515
  const pathLine = ` url: ${urlValue},
455
516
  `;
456
517
  return `${buildFunctionDoc2(operation)}export async function ${operation.functionName}<T = ${defaultResponseType}>(${requestArg}): Promise<T> {
457
- return request<T>({
518
+ ${destructureLine} return request<T>({
458
519
  ${pathLine} method: ${JSON.stringify(operation.method)},
459
520
  ${queryLine}${bodyLine} ...config,
460
521
  });
461
522
  }`;
462
523
  }
463
- function generateTsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
524
+ function generateTsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false, flattenOnGet = false) {
464
525
  const importTypes = [...operation.requestImportTypes, operation.responseTypeName, "RequestConfig"].filter(
465
526
  (value, index, array) => Boolean(value) && array.indexOf(value) === index
466
527
  );
@@ -476,10 +537,10 @@ function buildUrl(path?: Record<string, any>): string {
476
537
  import request from ${JSON.stringify(httpClientPath)};
477
538
  ${importLine}
478
539
  ${buildUrlHelper}
479
- ${renderFunction2(operation, false, mergeParams)}
540
+ ${renderFunction2(operation, false, mergeParams, flattenOnGet)}
480
541
  `;
481
542
  }
482
- function generateTsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false) {
543
+ function generateTsModuleFile(operations, httpClientPath, typeImportPath, mergeParams = false, flattenOnGet = false) {
483
544
  const importTypeSet = /* @__PURE__ */ new Set();
484
545
  for (const op of operations) {
485
546
  for (const t of op.requestImportTypes) importTypeSet.add(t);
@@ -494,7 +555,7 @@ function buildUrl(url: string, path?: Record<string, unknown>): string {
494
555
  return url.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
495
556
  }
496
557
  ` : "";
497
- const functions = operations.map((op) => renderFunction2(op, true, mergeParams)).join("\n\n");
558
+ const functions = operations.map((op) => renderFunction2(op, true, mergeParams, flattenOnGet)).join("\n\n");
498
559
  return `/* eslint-disable */
499
560
  // Auto-generated by swgto.
500
561
  import request from ${JSON.stringify(httpClientPath)};
@@ -718,7 +779,7 @@ async function generateFromConfig(cwd = process.cwd()) {
718
779
  for (const [controllerName, controllerOperations] of Object.entries(controllerMap)) {
719
780
  const relativeFile = path4.join(config.outputDir, moduleName, `${controllerName}.${config.outputType}`);
720
781
  const absoluteFile = path4.join(cwd, relativeFile);
721
- const content = config.outputType === "ts" ? generateTsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
782
+ const content = config.outputType === "ts" ? generateTsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams, config.flattenOnGet) : generateJsModuleFile(controllerOperations, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams, config.flattenOnGet);
722
783
  for (const op of controllerOperations) {
723
784
  op.fileBaseName = controllerName;
724
785
  }
@@ -729,7 +790,7 @@ async function generateFromConfig(cwd = process.cwd()) {
729
790
  for (const operation of moduleOperations) {
730
791
  const relativeFile = path4.join(config.outputDir, moduleName, `${operation.fileBaseName}.${config.outputType}`);
731
792
  const absoluteFile = path4.join(cwd, relativeFile);
732
- const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams);
793
+ const content = config.outputType === "ts" ? generateTsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams, config.flattenOnGet) : generateJsRequestFile(operation, config.httpClientPath, getRootImportPath(config.typeName), config.mergeParams, config.flattenOnGet);
733
794
  await writeTextFile(absoluteFile, content);
734
795
  files.push(relativeFile);
735
796
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generateFromConfig
4
- } from "./chunk-OMXGXES2.js";
4
+ } from "./chunk-PAFR76SO.js";
5
5
  import "./chunk-HBBM5HFP.js";
6
6
 
7
7
  // src/cli.ts
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ interface SwaggerTsConfig {
24
24
  fileNaming?: 'module' | 'path';
25
25
  flattenQueryParam?: boolean;
26
26
  mergeParams?: boolean;
27
+ flattenOnGet?: boolean;
27
28
  apiDocs?: ApiDocsConfig;
28
29
  }
29
30
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  generateFromConfig
3
- } from "./chunk-OMXGXES2.js";
3
+ } from "./chunk-PAFR76SO.js";
4
4
  import "./chunk-HBBM5HFP.js";
5
5
  export {
6
6
  generateFromConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swgto-ts",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Generate API request files from OpenAPI 3.x documents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,9 +25,6 @@
25
25
  "generator",
26
26
  "cli"
27
27
  ],
28
- "publishConfig": {
29
- "registry": "https://registry.npmjs.org/"
30
- },
31
28
  "engines": {
32
29
  "node": ">=18"
33
30
  },