From 4f6b4fc1942b1a5e38adc20fb2b793012498316f Mon Sep 17 00:00:00 2001 From: Bartosz Herba Date: Fri, 22 Nov 2024 13:43:10 +0100 Subject: [PATCH] feat: add form data handler in the sdk middleware module --- .changeset/long-panthers-learn.md | 20 +++++++ .../__tests__/__mocks__/apiClient/server.js | 3 ++ .../__tests__/__mocks__/apiClient/types.ts | 6 +++ .../modules/middlewareModule.spec.ts | 52 +++++++++++++++++++ .../utils/getRequestSender.ts | 27 +++++++++- 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 .changeset/long-panthers-learn.md diff --git a/.changeset/long-panthers-learn.md b/.changeset/long-panthers-learn.md new file mode 100644 index 0000000000..e05c120055 --- /dev/null +++ b/.changeset/long-panthers-learn.md @@ -0,0 +1,20 @@ +--- +"@vue-storefront/sdk": minor +--- + +**[ADDED]** Add support for multipart/form-data requests in SDK + +- Added handling for multipart/form-data content type in the default HTTP client +- Automatically handles File and Blob objects in request parameters + +```typescript +// Upload a file using multipart/form-data +await sdk.commerce.uploadFile( + { file: new File(["content"], "test.txt", { type: "text/plain" }) }, + prepareConfig({ + headers: { + "Content-Type": "multipart/form-data", + }, + }) +); +``` diff --git a/packages/sdk/src/__tests__/__mocks__/apiClient/server.js b/packages/sdk/src/__tests__/__mocks__/apiClient/server.js index 747f62408f..60057c9dfe 100644 --- a/packages/sdk/src/__tests__/__mocks__/apiClient/server.js +++ b/packages/sdk/src/__tests__/__mocks__/apiClient/server.js @@ -23,6 +23,9 @@ const { createApiClient } = apiClientFactory({ unauthorized: async (_context, _params) => { throw { statusCode: 401, message: "Unauthorized" }; }, + uploadFile: async (_context, params) => { + return { file: params.file }; + }, logout: async (_context) => {}, }, }); diff --git a/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts b/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts index 308e92b809..966a4bff5d 100644 --- a/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts +++ b/packages/sdk/src/__tests__/__mocks__/apiClient/types.ts @@ -21,4 +21,10 @@ export type Endpoints = { * For testing void responses. */ logout: () => Promise; + /** + * Upload a file. + */ + uploadFile: (params: { + file: { name: string; content: string }; + }) => Promise<{ file: { name: string; content: string } }>; }; diff --git a/packages/sdk/src/__tests__/integration/modules/middlewareModule.spec.ts b/packages/sdk/src/__tests__/integration/modules/middlewareModule.spec.ts index f5fd893a2f..a348aff82e 100644 --- a/packages/sdk/src/__tests__/integration/modules/middlewareModule.spec.ts +++ b/packages/sdk/src/__tests__/integration/modules/middlewareModule.spec.ts @@ -867,4 +867,56 @@ describe("middlewareModule", () => { expect(logSpy).not.toHaveBeenCalled(); }); + + it("should create FormData when Content-Type is multipart/form-data", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(middlewareModule, { + apiUrl: "http://localhost:8181/commerce", + cdnCacheBustingId: "commit-hash", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.uploadFile( + { file: { name: "test.txt", content: "test" } }, + prepareConfig({ + headers: { + "Content-Type": "multipart/form-data", + }, + }) + ); + + const [url, params, config] = customHttpClient.mock.calls[0]; + expect(url).toBe("http://localhost:8181/commerce/uploadFile"); + expect(params[0]).toEqual({ file: { name: "test.txt", content: "test" } }); + expect(config.headers["Content-Type"]).toBe("multipart/form-data"); + }); + + it("should maintain correct endpoint URL for multipart requests", async () => { + const customHttpClient = jest.fn(); + const sdkConfig = { + commerce: buildModule(middlewareModule, { + apiUrl: "http://localhost:8181/commerce", + httpClient: customHttpClient, + }), + }; + const sdk = initSDK(sdkConfig); + + await sdk.commerce.uploadFile( + { file: { name: "test.txt", content: "test" } }, + prepareConfig({ + headers: { + "Content-Type": "multipart/form-data", + }, + }) + ); + + expect(customHttpClient).toHaveBeenCalledWith( + "http://localhost:8181/commerce/uploadFile", + expect.any(Array), + expect.any(Object) + ); + }); }); diff --git a/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts b/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts index d04b9ef4d1..a65d8653aa 100644 --- a/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts +++ b/packages/sdk/src/modules/middlewareModule/utils/getRequestSender.ts @@ -108,9 +108,34 @@ export const getRequestSender = (options: Options): RequestSender => { }; const defaultHTTPClient: HTTPClient = async (url, params, config) => { + const isMultipart = config?.headers?.["Content-Type"]?.includes( + "multipart/form-data" + ); + + let body; + if (config?.method === "GET") { + body = undefined; + } else if (isMultipart) { + const formData = new FormData(); + Object.entries(params).forEach(([key, value]) => { + if (value instanceof Blob || value instanceof File) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }); + body = formData; + + if (config?.headers) { + delete config.headers["Content-Type"]; + } + } else { + body = JSON.stringify(params); + } + const response = await fetch(url, { ...config, - body: config?.method === "GET" ? undefined : JSON.stringify(params), + body, credentials: "include", });