From 71691bccc433b7613632dfcce6aedfba040191a1 Mon Sep 17 00:00:00 2001 From: Tim Jacobs Date: Wed, 20 Nov 2024 14:29:40 +0100 Subject: [PATCH 1/4] feat: add onRequest to ApiClientHooks and add to onRequest interceptor --- packages/api-client/src/createAPIClient.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/api-client/src/createAPIClient.ts b/packages/api-client/src/createAPIClient.ts index 2aa78eff7..5a98bafa6 100644 --- a/packages/api-client/src/createAPIClient.ts +++ b/packages/api-client/src/createAPIClient.ts @@ -1,4 +1,11 @@ -import { type FetchResponse, ofetch, FetchOptions } from "ofetch"; +import { + type FetchResponse, + type FetchRequest, + ofetch, + type FetchOptions, + type ResolvedFetchOptions, + type ResponseType, +} from "ofetch"; import type { operations } from "../api-types/storeApiTypes"; import { ClientHeaders, createHeaders } from "./defaultHeaders"; import { errorInterceptor } from "./errorInterceptor"; @@ -58,6 +65,10 @@ export type ApiClientHooks = { onResponseError: (response: FetchResponse) => void; onSuccessResponse: (response: FetchResponse) => void; onDefaultHeaderChanged: (headerName: string, value?: T) => void; + onRequest: ( + request: FetchRequest, + options: ResolvedFetchOptions, + ) => void; }; export function createAPIClient< @@ -108,6 +119,9 @@ export function createAPIClient< apiClientHooks.callHook("onResponseError", response); errorInterceptor(response); }, + async onRequest({ request, options }) { + await apiClientHooks.callHook("onRequest", request, options); + }, }); /** * Invoke API request based on provided path definition. From 64b4c43d0415a205b8ae512e19e2fa4efced7690 Mon Sep 17 00:00:00 2001 From: Tim Jacobs Date: Wed, 20 Nov 2024 15:16:37 +0100 Subject: [PATCH 2/4] test: fully test onRequest --- .../api-client/src/createApiClient.test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/packages/api-client/src/createApiClient.test.ts b/packages/api-client/src/createApiClient.test.ts index 2cc94b01d..ac192ca87 100644 --- a/packages/api-client/src/createApiClient.test.ts +++ b/packages/api-client/src/createApiClient.test.ts @@ -200,6 +200,130 @@ describe("createAPIClient", () => { expect(contextChangedMock).not.toHaveBeenCalled(); }); + it("should invoke onRequest hook once", async () => { + const app = createApp().use( + "/context", + eventHandler(async () => { + return {}; + }), + ); + + const baseURL = await createPortAndGetUrl(app); + + const onRequestMock = vi.fn(); + + const client = createAPIClient({ + baseURL, + accessToken: "123", + contextToken: "456", + }); + + client.hook("onRequest", onRequestMock); + + await client.invoke("readContext get /context"); + + expect(onRequestMock).toHaveBeenCalledOnce(); + }); + + it("should allow onRequest hook to modify request options", async () => { + const requestHeadersSpy = vi.fn(); + + const app = createApp().use( + "/context", + eventHandler(async (event) => { + const requestHeaders = getHeaders(event); + requestHeadersSpy(requestHeaders); + return {}; + }), + ); + + const baseURL = await createPortAndGetUrl(app); + + const client = createAPIClient({ baseURL }); + + client.hook("onRequest", (_request, options) => { + const existingHeaders = new Headers(options.headers); + existingHeaders.set("x-custom-header", "modified-header"); + options.headers = existingHeaders; + }); + + await client.invoke("readContext get /context"); + + expect(requestHeadersSpy).toHaveBeenCalledWith( + expect.objectContaining({ + "x-custom-header": "modified-header", + }), + ); + }); + + it("should invoke all onRequest hooks independently", async () => { + const requestHeadersSpy = vi.fn(); + + const app = createApp().use( + "/context", + eventHandler(async (event) => { + const requestHeaders = getHeaders(event); + requestHeadersSpy(requestHeaders); // Capture request headers + return {}; + }), + ); + + const baseURL = await createPortAndGetUrl(app); + + const client = createAPIClient({ baseURL }); + + client.hook("onRequest", (_request, options) => { + const existingHeaders = new Headers(options.headers); + existingHeaders.set("x-hook1", "value1"); + options.headers = existingHeaders; + }); + + client.hook("onRequest", (_request, options) => { + const existingHeaders = new Headers(options.headers); + existingHeaders.set("x-hook2", "value2"); + options.headers = existingHeaders; + }); + + await client.invoke("readContext get /context"); + + expect(requestHeadersSpy).toHaveBeenCalledWith( + expect.objectContaining({ + "x-hook1": "value1", + "x-hook2": "value2", + }), + ); + }); + + it("should not interfere with normal request execution", async () => { + const requestHeadersSpy = vi.fn(); + + const app = createApp().use( + "/context", + eventHandler(async (event) => { + const requestHeaders = getHeaders(event); + requestHeadersSpy(requestHeaders); + return { success: true }; + }), + ); + + const baseURL = await createPortAndGetUrl(app); + + const client = createAPIClient({ baseURL }); + + client.hook("onRequest", () => { + // Perform a no-op + }); + + const response = await client.invoke("readContext get /context"); + + expect(response.data).toEqual({ success: true }); + expect(requestHeadersSpy).toHaveBeenCalledWith( + expect.objectContaining({ + accept: "application/json", + }), + ); + }); + it("should throw error when there is a problem with request", async () => { const app = createApp().use( "/checkout/cart", From 71744108f9ed82278106d9286fc262679e718a29 Mon Sep 17 00:00:00 2001 From: Tim Jacobs Date: Wed, 20 Nov 2024 15:33:56 +0100 Subject: [PATCH 3/4] doc: add documentation, improve testcases --- packages/api-client/README.md | 10 ++++++++++ packages/api-client/src/createApiClient.test.ts | 12 +++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/api-client/README.md b/packages/api-client/README.md index 4df1a2e3e..a2cc915d4 100644 --- a/packages/api-client/README.md +++ b/packages/api-client/README.md @@ -265,6 +265,16 @@ apiClient.hook("onDefaultHeaderChanged", (key, value) => { calling `apiClient.hook` will autocomplete the list of available hooks. +#### onResponse + +Using the onRequest hook, you can modify the request before it is sent. This is particularly useful for adding custom headers. + +```typescript +client.hook("onRequest", (_request, options) => { + options.headers.append("x-custom-header", "value"); +}); +``` + ## Links - [🧑‍🎓 Tutorial](https://api-client-tutorial-composable-frontends.pages.dev) diff --git a/packages/api-client/src/createApiClient.test.ts b/packages/api-client/src/createApiClient.test.ts index ac192ca87..db2f8492e 100644 --- a/packages/api-client/src/createApiClient.test.ts +++ b/packages/api-client/src/createApiClient.test.ts @@ -242,9 +242,7 @@ describe("createAPIClient", () => { const client = createAPIClient({ baseURL }); client.hook("onRequest", (_request, options) => { - const existingHeaders = new Headers(options.headers); - existingHeaders.set("x-custom-header", "modified-header"); - options.headers = existingHeaders; + options.headers.append("x-custom-header", "modified-header"); }); await client.invoke("readContext get /context"); @@ -273,15 +271,11 @@ describe("createAPIClient", () => { const client = createAPIClient({ baseURL }); client.hook("onRequest", (_request, options) => { - const existingHeaders = new Headers(options.headers); - existingHeaders.set("x-hook1", "value1"); - options.headers = existingHeaders; + options.headers.append("x-hook1", "value1"); }); client.hook("onRequest", (_request, options) => { - const existingHeaders = new Headers(options.headers); - existingHeaders.set("x-hook2", "value2"); - options.headers = existingHeaders; + options.headers.append("x-hook2", "value2"); }); await client.invoke("readContext get /context"); From 0450434dac246cf1767ce366d2d296317067a985 Mon Sep 17 00:00:00 2001 From: Tim Jacobs Date: Wed, 20 Nov 2024 15:44:41 +0100 Subject: [PATCH 4/4] doc: changeset --- .changeset/bright-rivers-explain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-rivers-explain.md diff --git a/.changeset/bright-rivers-explain.md b/.changeset/bright-rivers-explain.md new file mode 100644 index 000000000..ffc33f0a9 --- /dev/null +++ b/.changeset/bright-rivers-explain.md @@ -0,0 +1,5 @@ +--- +"@shopware/api-client": minor +--- + +Added onRequest interceptor as option to ApiClientHooks in API Client