Skip to content

Commit 4123907

Browse files
fix: fix bug #525 (#533)
* fix: fix bug #525 * chore: clean code
1 parent 16b4d31 commit 4123907

File tree

10 files changed

+280
-8
lines changed

10 files changed

+280
-8
lines changed

.changeset/nice-tips-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openapi-ts-request': patch
3+
---
4+
5+
fix: fix bug #525

README-en_US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ openapi -i ./spec.json -o ./apis
252252
| isOnlyGenTypeScriptType | no | boolean | false | only generate typescript type |
253253
| isCamelCase | no | boolean | true | camelCase naming of controller files and request client |
254254
| isSupportParseEnumDesc | no | boolean | false | parse enum description to generate enum label, format example: `UserRole:User(Normal User)=0,Agent(Agent)=1,Admin(Administrator)=2` |
255+
| binaryMediaTypes | no | string[] | - | custom binary media types list, default includes: ['application/octet-stream', 'application/pdf', 'image/*', 'video/*', 'audio/*'] |
255256
| hook | no | [Custom Hook](#Custom-Hook) | - | custom hook |
256257

257258
## Custom Hook

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
<!-- TODO:需要修改文档, 添加参数, 添加apifox的配置支持 -->
2-
31
## 介绍
42

53
[![GitHub Repo stars](https://img.shields.io/github/stars/openapi-ui/openapi-ts-request?style=social)](https://github.com/openapi-ui/openapi-ts-request) [![npm (scoped)](https://img.shields.io/npm/v/openapi-ts-request)](https://www.npmjs.com/package/openapi-ts-request) ![GitHub tag](https://img.shields.io/github/v/tag/openapi-ui/openapi-ts-request?include_prereleases)
@@ -50,8 +48,6 @@ pnpm i openapi-ts-request -D
5048
import type { GenerateServiceProps } from 'openapi-ts-request';
5149

5250
export default {
53-
// schemaPath: './openapi.json', // 本地openapi文件
54-
// serversPath: './src/apis', // 接口存放路径
5551
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
5652
} as GenerateServiceProps;
5753
```
@@ -254,6 +250,7 @@ openapi --i ./spec.json --o ./apis
254250
| isOnlyGenTypeScriptType || boolean | false | 仅生成 typescript 类型 |
255251
| isCamelCase || boolean | true | 小驼峰命名文件和请求函数 |
256252
| isSupportParseEnumDesc || boolean | false | 解析枚举描述生成枚举标签,格式参考:`系统用户角色:User(普通用户)=0,Agent(经纪人)=1,Admin(管理员)=2` |
253+
| binaryMediaTypes || string[] | - | 自定义二进制媒体类型列表,默认包含:['application/octet-stream', 'application/pdf', 'image/*', 'video/*', 'audio/*'] |
257254
| hook || [Custom Hook](#Custom-Hook) | - | 自定义 hook |
258255

259256
## 自定义 Hook

src/generator/serviceGenarator.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { existsSync, readFileSync } from 'fs';
22
import { globSync } from 'glob';
33
import type { Dictionary } from 'lodash';
44
import {
5-
// camelCase,
65
entries,
76
filter,
87
find,
@@ -81,7 +80,10 @@ import { type MergeOption } from './type';
8180
import {
8281
capitalizeFirstLetter,
8382
genDefaultFunctionName,
83+
getAxiosResponseType,
8484
getBasePrefix,
85+
getBinaryMediaTypes,
86+
getBinaryResponseType,
8587
getDefaultFileTag,
8688
getDefaultType,
8789
getFinalFileName,
@@ -92,13 +94,13 @@ import {
9294
isAllNumeric,
9395
isArraySchemaObject,
9496
isBinaryArraySchemaObject,
97+
isBinaryMediaType,
9598
isNonArraySchemaObject,
9699
isReferenceObject,
97100
isSchemaObject,
98101
markAllowedSchema,
99102
parseDescriptionEnum,
100103
replaceDot,
101-
// resolveFunctionName,
102104
resolveRefs,
103105
resolveTypeName,
104106
} from './util';
@@ -1152,6 +1154,7 @@ export default class ServiceGenerator {
11521154
mediaType: '*/*',
11531155
type: 'unknown',
11541156
isAnonymous: false,
1157+
responseType: undefined as string | undefined,
11551158
};
11561159

11571160
if (!response) {
@@ -1160,9 +1163,16 @@ export default class ServiceGenerator {
11601163

11611164
const resContent: ContentObject | undefined = response.content;
11621165
const resContentMediaTypes = keys(resContent);
1166+
1167+
// 检测二进制流媒体类型
1168+
const binaryMediaTypes = getBinaryMediaTypes(this.config.binaryMediaTypes);
1169+
const binaryMediaType = resContentMediaTypes.find((mediaType) =>
1170+
isBinaryMediaType(mediaType, binaryMediaTypes)
1171+
);
1172+
11631173
const mediaType = resContentMediaTypes.includes('application/json')
11641174
? 'application/json'
1165-
: resContentMediaTypes[0]; // 优先使用 application/json
1175+
: binaryMediaType || resContentMediaTypes[0]; // 优先使用 application/json,然后是二进制类型
11661176

11671177
if (!isObject(resContent) || !mediaType) {
11681178
return defaultResponse;
@@ -1174,8 +1184,20 @@ export default class ServiceGenerator {
11741184
mediaType,
11751185
type: 'unknown',
11761186
isAnonymous: false,
1187+
responseType: undefined as string | undefined,
11771188
};
11781189

1190+
// 如果是二进制媒体类型,直接返回二进制类型
1191+
if (isBinaryMediaType(mediaType, binaryMediaTypes)) {
1192+
const binaryType = getBinaryResponseType();
1193+
responseSchema.type = binaryType;
1194+
1195+
// 自动为二进制响应添加 responseType 配置
1196+
responseSchema.responseType = getAxiosResponseType(binaryType);
1197+
1198+
return responseSchema;
1199+
}
1200+
11791201
if (isReferenceObject(schema)) {
11801202
const refName = getLastRefName(schema.$ref);
11811203
const childrenSchema = components.schemas[refName];
@@ -1306,14 +1328,26 @@ export default class ServiceGenerator {
13061328

13071329
const resContent: ContentObject = response.content;
13081330
const resContentMediaTypes = keys(resContent);
1331+
1332+
// 检测二进制流媒体类型
1333+
const binaryMediaTypes = getBinaryMediaTypes(this.config.binaryMediaTypes);
1334+
const binaryMediaType = resContentMediaTypes.find((mediaType) =>
1335+
isBinaryMediaType(mediaType, binaryMediaTypes)
1336+
);
1337+
13091338
const mediaType = resContentMediaTypes.includes('application/json')
13101339
? 'application/json'
1311-
: resContentMediaTypes[0];
1340+
: binaryMediaType || resContentMediaTypes[0];
13121341

13131342
if (!isObject(resContent) || !mediaType) {
13141343
return 'unknown';
13151344
}
13161345

1346+
// 如果是二进制媒体类型,直接返回二进制类型
1347+
if (isBinaryMediaType(mediaType, binaryMediaTypes)) {
1348+
return getBinaryResponseType();
1349+
}
1350+
13171351
let schema = (resContent[mediaType].schema ||
13181352
DEFAULT_SCHEMA) as SchemaObject;
13191353

src/generator/util.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,77 @@ export const parseDescriptionEnum = (
572572

573573
return enumMap;
574574
};
575+
576+
/**
577+
* 获取默认的二进制媒体类型列表
578+
*/
579+
export const getDefaultBinaryMediaTypes = (): string[] => {
580+
return [
581+
'application/octet-stream',
582+
'application/pdf',
583+
'application/zip',
584+
'application/x-zip-compressed',
585+
'image/*',
586+
'video/*',
587+
'audio/*',
588+
];
589+
};
590+
591+
/**
592+
* 获取二进制媒体类型列表
593+
* 支持配置自定义二进制媒体类型
594+
* @param customBinaryTypes 自定义二进制媒体类型列表
595+
*/
596+
export const getBinaryMediaTypes = (
597+
customBinaryTypes: string[] = []
598+
): string[] => {
599+
const defaultBinaryTypes = getDefaultBinaryMediaTypes();
600+
return [...defaultBinaryTypes, ...customBinaryTypes];
601+
};
602+
603+
/**
604+
* 检测是否为二进制媒体类型
605+
* @param mediaType 媒体类型
606+
* @param binaryMediaTypes 二进制媒体类型列表
607+
*/
608+
export const isBinaryMediaType = (
609+
mediaType: string,
610+
binaryMediaTypes: string[]
611+
): boolean => {
612+
return binaryMediaTypes.some((type) => {
613+
if (type.endsWith('/*')) {
614+
// 处理通配符类型,如 image/*, video/*
615+
const prefix = type.slice(0, -1);
616+
return mediaType.startsWith(prefix);
617+
}
618+
return mediaType === type;
619+
});
620+
};
621+
622+
/**
623+
* 获取二进制响应类型
624+
* 默认返回 Blob,这是浏览器环境中最常用的二进制类型
625+
*/
626+
export const getBinaryResponseType = (): string => {
627+
return 'Blob';
628+
};
629+
630+
/**
631+
* 获取 axios responseType 配置
632+
* 根据二进制响应类型返回对应的 responseType
633+
* @param binaryType 二进制类型
634+
*/
635+
export const getAxiosResponseType = (binaryType: string): string => {
636+
switch (binaryType.toLowerCase()) {
637+
case 'blob':
638+
return 'blob';
639+
case 'arraybuffer':
640+
return 'arraybuffer';
641+
case 'uint8array':
642+
return 'arraybuffer'; // Uint8Array 需要从 ArrayBuffer 转换
643+
case 'buffer':
644+
return 'arraybuffer'; // Node.js Buffer 需要从 ArrayBuffer 转换
645+
default:
646+
return 'blob';
647+
}
648+
};

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ export type GenerateServiceProps = {
172172
* 多网关唯一标识
173173
*/
174174
uniqueKey?: string;
175+
/**
176+
* 自定义二进制媒体类型列表
177+
* 默认包含: ['application/octet-stream', 'application/pdf', 'image/*', 'video/*', 'audio/*']
178+
*/
179+
binaryMediaTypes?: string[];
175180
/**
176181
* 自定义 hook
177182
*/

templates/serviceController.njk

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@
128128
return request{{ ("<" + api.response.type + ">") | safe if genType === "ts" }}('{{ api.path }}', {
129129
{% endif -%}
130130
method: '{{ api.method | upper }}',
131+
{%- if api.response.responseType %}
132+
responseType: '{{ api.response.responseType }}',
133+
{%- endif %}
131134
{%- if api.hasHeader and api.body.mediaType %}
132135
headers: {
133136
{%- if api.body.mediaType %}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable */
2+
// @ts-ignore
3+
import { queryOptions, useMutation } from '@tanstack/react-query';
4+
import type { DefaultError } from '@tanstack/react-query';
5+
import request from 'axios';
6+
7+
import * as apis from './fileId';
8+
import * as API from './types';
9+
10+
/** 下载文件 根据文件ID下载对应的文件流。 GET /files/${param0}/download */
11+
export function filesFileIdDownloadUsingGetQueryOptions(options: {
12+
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
13+
params: API.FilesFileIdDownloadUsingGetParams;
14+
options?: { [key: string]: unknown };
15+
}) {
16+
return queryOptions({
17+
queryFn: ({ queryKey }) => {
18+
return apis.filesFileIdDownloadUsingGet(queryKey[1] as typeof options);
19+
},
20+
queryKey: ['filesFileIdDownloadUsingGet', options],
21+
});
22+
}
23+
/* eslint-disable */
24+
// @ts-ignore
25+
import request from 'axios';
26+
27+
import * as API from './types';
28+
29+
/** 下载文件 根据文件ID下载对应的文件流。 GET /files/${param0}/download */
30+
export function filesFileIdDownloadUsingGet({
31+
params,
32+
options,
33+
}: {
34+
// 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
35+
params: API.FilesFileIdDownloadUsingGetParams;
36+
options?: { [key: string]: unknown };
37+
}) {
38+
const { fileId: param0, ...queryParams } = params;
39+
40+
return request<Blob>(`/files/${param0}/download`, {
41+
method: 'GET',
42+
responseType: 'blob',
43+
params: { ...queryParams },
44+
...(options || {}),
45+
});
46+
}
47+
/* eslint-disable */
48+
// @ts-ignore
49+
export * from './types';
50+
51+
export * from './fileId';
52+
export * from './fileId.reactquery';
53+
/* eslint-disable */
54+
// @ts-ignore
55+
56+
export type FilesFileIdDownloadUsingGetParams = {
57+
/** 文件的唯一标识符 */
58+
fileId: string;
59+
};
60+
61+
export type FilesFileIdDownloadUsingGetResponses = {
62+
/**
63+
* 文件下载成功
64+
*/
65+
200: Blob;
66+
/**
67+
* 文件未找到
68+
*/
69+
404: {
70+
code?: number;
71+
message?: string;
72+
};
73+
};

test/common.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,18 @@ export async function ${api.functionName}(${api.body ? `data: ${api.body.type}`
497497
readGeneratedFiles('./apis/components-parameters')
498498
).resolves.toMatchFileSnapshot(getSnapshotDir(ctx));
499499
});
500+
501+
it('测试文件下载 API', async (ctx) => {
502+
await openAPI.generateService({
503+
schemaPath: join(
504+
import.meta.dirname,
505+
'./example-files/openapi-file-download.json'
506+
),
507+
serversPath: './apis/file-download',
508+
isGenReactQuery: true,
509+
});
510+
await expect(
511+
readGeneratedFiles('./apis/file-download')
512+
).resolves.toMatchFileSnapshot(getSnapshotDir(ctx));
513+
});
500514
});

0 commit comments

Comments
 (0)