Skip to content

Commit 7372bee

Browse files
feat(core): support for multipart/form-data file array (#572)
Co-authored-by: Youssef Gaber <1728215+Gabrola@users.noreply.github.com>
1 parent 7108c19 commit 7372bee

5 files changed

Lines changed: 60 additions & 3 deletions

File tree

.changeset/pretty-spoons-dream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/core': minor
3+
---
4+
5+
Allow arrays of files to be uploaded

libs/ts-rest/core/src/lib/client.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ export const router = c.router(
148148
},
149149
contentType: 'multipart/form-data',
150150
},
151+
uploadArray: {
152+
method: 'POST',
153+
path: '/upload-array',
154+
body: c.type<{ files: File[] }>(),
155+
responses: {
156+
200: c.type<{ message: string }>(),
157+
},
158+
contentType: 'multipart/form-data',
159+
},
151160
},
152161
{
153162
baseHeaders: z.object({
@@ -628,6 +637,38 @@ describe('client', () => {
628637
},
629638
});
630639
});
640+
641+
it('w/ File Array', async () => {
642+
const value = { key: 'value' };
643+
fetchMock.postOnce(
644+
{
645+
url: 'https://api.com/upload-array',
646+
},
647+
{ body: value, status: 200 },
648+
);
649+
650+
const files = [
651+
new File([''], 'filename-1', { type: 'text/plain' }),
652+
new File([''], 'filename-2', { type: 'text/plain' }),
653+
];
654+
655+
const result = await client.uploadArray({
656+
body: { files },
657+
});
658+
659+
expect(result.body).toStrictEqual(value);
660+
expect(result.status).toBe(200);
661+
expect(result.headers.get('Content-Length')).toBe('15');
662+
expect(result.headers.get('Content-Type')).toBe('application/json');
663+
664+
expect(fetchMock).toHaveLastFetched(true, {
665+
matcher: (_, options) => {
666+
const formData = options.body as FormData;
667+
const formDataFiles = formData.getAll('files');
668+
return formDataFiles[0] === files[0] && formDataFiles[1] === files[1];
669+
},
670+
});
671+
});
631672
});
632673

633674
describe('application/x-www-form-urlencoded', () => {

libs/ts-rest/core/src/lib/client.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,22 @@ export const tsRestFetchApi: ApiFetcher = async ({
156156
const createFormData = (body: unknown) => {
157157
const formData = new FormData();
158158

159-
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
159+
const appendToFormData = (key: string, value: unknown) => {
160160
if (value instanceof File) {
161161
formData.append(key, value);
162162
} else {
163163
formData.append(key, JSON.stringify(value));
164164
}
165+
};
166+
167+
Object.entries(body as Record<string, unknown>).forEach(([key, value]) => {
168+
if (Array.isArray(value)) {
169+
for (const item of value) {
170+
appendToFormData(key, item);
171+
}
172+
} else {
173+
appendToFormData(key, value);
174+
}
165175
});
166176

167177
return formData;

libs/ts-rest/core/src/lib/infer-types.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const contract = c.router(
5959
method: 'POST',
6060
path: '/images',
6161
contentType: 'multipart/form-data',
62-
body: c.body<{ image: File }>(),
62+
body: c.type<{ image: File; images: File[] }>(),
6363
responses: {
6464
201: c.otherResponse({
6565
contentType: 'text/plain',
@@ -568,6 +568,7 @@ it('type inference helpers', () => {
568568
body:
569569
| {
570570
image: File;
571+
images: File[];
571572
}
572573
| FormData;
573574
headers: { authorization: string; age?: number };

libs/ts-rest/core/src/lib/infer-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export type ClientInferResponseBody<
148148

149149
type BodyWithoutFileIfMultiPart<T extends AppRouteMutation> =
150150
T['contentType'] extends 'multipart/form-data'
151-
? Without<ZodInferOrType<T['body']>, File>
151+
? Without<ZodInferOrType<T['body']>, File | File[]>
152152
: ZodInferOrType<T['body']>;
153153

154154
export type ServerInferRequest<

0 commit comments

Comments
 (0)