Skip to content

Commit 4f9c219

Browse files
Handle formBody schema fields other than string or file (#1313)
Co-authored-by: Arda TANRIKULU <[email protected]>
1 parent 1655681 commit 4f9c219

File tree

8 files changed

+140
-31
lines changed

8 files changed

+140
-31
lines changed

.changeset/flat-pans-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"fets": minor
3+
---
4+
5+
Support for multipart fields other than string or file

packages/fets/src/client/createClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { iterateAsyncVoid } from '@whatwg-node/server';
44
import { EMPTY_OBJECT } from '../plugins/utils.js';
55
import { HTTPMethod } from '../typed-fetch.js';
66
import { OpenAPIDocument, Router, SecurityScheme } from '../types.js';
7+
import { isBlob } from '../utils.js';
78
import { createClientTypedResponsePromise } from './clientResponse.js';
89
import {
910
ClientMethod,
@@ -213,7 +214,7 @@ export function createClient({
213214
for (const key in formDataBody) {
214215
const value = formDataBody[key];
215216
if (value != null) {
216-
requestInit.body.append(key, value);
217+
requestInit.body.append(key, isBlob(value) ? value : value.toString());
217218
}
218219
}
219220
}

packages/fets/src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ export function asyncIterationUntilReturn<TInput, TOutput>(
2828
}
2929
return iterate();
3030
}
31+
32+
export function isBlob(value: any): value is Blob {
33+
return value.arrayBuffer !== undefined;
34+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient, type NormalizeOAS } from 'fets';
2+
import { File, Request, Response } from '@whatwg-node/fetch';
3+
import type clientFormDataOAS from './fixtures/example-formdata';
4+
5+
describe('Client', () => {
6+
describe('POST', () => {
7+
type NormalizedOAS = NormalizeOAS<typeof clientFormDataOAS>;
8+
const client = createClient<NormalizedOAS>({
9+
endpoint: 'https://postman-echo.com',
10+
async fetchFn(info, init) {
11+
const request = new Request(info, init);
12+
const formdataReq = await request.formData();
13+
return Response.json({
14+
formdata: Object.fromEntries(formdataReq.entries()),
15+
});
16+
},
17+
});
18+
it('handles formdata with non-string values', async () => {
19+
const response = await client['/post'].post({
20+
formData: {
21+
blob: new File(['foo'], 'foo.txt'),
22+
boolean: true,
23+
number: 42,
24+
},
25+
});
26+
const resJson = await response.json();
27+
expect(resJson.formdata).toMatchObject({
28+
boolean: 'true',
29+
number: '42',
30+
});
31+
});
32+
});
33+
});

packages/fets/tests/client/client-query-serialization.spec.ts

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,41 @@ import { createClient, type NormalizeOAS } from 'fets';
22
import { Request, Response } from '@whatwg-node/fetch';
33
import type clientQuerySerializationOAS from './fixtures/example-client-query-serialization-oas';
44

5-
type NormalizedOAS = NormalizeOAS<typeof clientQuerySerializationOAS>;
6-
75
describe('Client', () => {
8-
const client = createClient<NormalizedOAS>({
9-
endpoint: 'https://postman-echo.com',
10-
fetchFn(info, init) {
11-
const request = new Request(info.toString(), init);
12-
return Promise.resolve(
13-
Response.json({
14-
url: request.url,
15-
}),
16-
);
17-
},
18-
});
19-
it('should support deep objects in query', async () => {
20-
const response = await client['/get'].get({
21-
query: {
22-
shallow: 'foo',
23-
deep: {
24-
key1: 'bar',
25-
key2: 'baz',
26-
},
27-
array: ['qux', 'quux'],
6+
describe('GET', () => {
7+
type NormalizedOAS = NormalizeOAS<typeof clientQuerySerializationOAS>;
8+
const client = createClient<NormalizedOAS>({
9+
endpoint: 'https://postman-echo.com',
10+
fetchFn(info, init) {
11+
const request = new Request(info.toString(), init);
12+
return Promise.resolve(
13+
Response.json({
14+
url: request.url,
15+
}),
16+
);
2817
},
2918
});
19+
it('should support deep objects in query', async () => {
20+
const response = await client['/get'].get({
21+
query: {
22+
shallow: 'foo',
23+
deep: {
24+
key1: 'bar',
25+
key2: 'baz',
26+
},
27+
array: ['qux', 'quux'],
28+
},
29+
});
3030

31-
const resJson = await response.json();
31+
const resJson = await response.json();
3232

33-
expect(resJson.url).toBe(
34-
'https://postman-echo.com/get?shallow=foo&deep%5Bkey1%5D=bar&deep%5Bkey2%5D=baz&array=qux&array=quux',
35-
);
36-
});
37-
it('lazily handles json', async () => {
38-
const resJson = await client['/get'].get().json();
39-
expect(resJson.url).toBe('https://postman-echo.com/get');
33+
expect(resJson.url).toBe(
34+
'https://postman-echo.com/get?shallow=foo&deep%5Bkey1%5D=bar&deep%5Bkey2%5D=baz&array=qux&array=quux',
35+
);
36+
});
37+
it('lazily handles json', async () => {
38+
const resJson = await client['/get'].get().json();
39+
expect(resJson.url).toBe('https://postman-echo.com/get');
40+
});
4041
});
4142
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* eslint-disable */
2+
export default {
3+
openapi: '3.0.3',
4+
servers: ['https://postman-echo.com'],
5+
paths: {
6+
'/post': {
7+
post: {
8+
requestBody: {
9+
content: {
10+
'multipart/form-data': {
11+
schema: {
12+
type: 'object',
13+
properties: {
14+
blob: {
15+
type: 'string',
16+
format: 'binary',
17+
},
18+
boolean: {
19+
type: 'boolean',
20+
},
21+
number: {
22+
type: 'number',
23+
},
24+
},
25+
additionalProperties: false,
26+
required: ['blob', 'boolean', 'number'],
27+
},
28+
},
29+
},
30+
required: true,
31+
},
32+
responses: {
33+
'200': {
34+
description: '',
35+
content: {
36+
'application/json; charset=utf-8': {
37+
schema: {
38+
type: 'object',
39+
properties: {
40+
formdata: {
41+
type: 'object',
42+
properties: {
43+
blob: {
44+
type: 'string',
45+
},
46+
boolean: {
47+
type: 'boolean',
48+
},
49+
number: {
50+
type: 'number',
51+
},
52+
},
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
},
61+
},
62+
},
63+
} as const;

packages/fets/tests/client/fixtures/example-oas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export default {
211211
properties: {
212212
file: { type: 'string', format: 'binary' },
213213
description: { type: 'string', maxLength: 255 },
214+
licensed: { type: 'boolean' },
214215
},
215216
required: ['file'],
216217
additionalProperties: false,

packages/fets/tests/client/oas-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const uploadRes = await client['/upload'].post({
8080
formData: {
8181
file: new File(['Hello world'], 'hello.txt'),
8282
description: 'Greetings',
83+
licensed: true,
8384
},
8485
});
8586

0 commit comments

Comments
 (0)