From 2a4b067f43f7e0b75aecbf5c2fb3013a4e96e591 Mon Sep 17 00:00:00 2001 From: Gruak <43846312+Gruak@users.noreply.github.com> Date: Mon, 19 Aug 2024 00:45:33 +0200 Subject: [PATCH] feat(openapi-fetch): baseUrl per request (#1817) * feat(openapi-fetch): baseUrl per request * chore: update docs * chore: add changeset * fix(openapi-fetch): lint error * Update .changeset/happy-singers-fry.md Co-authored-by: Martin Paucot * test: fix typo --------- Co-authored-by: Martin Paucot --- .changeset/happy-singers-fry.md | 5 +++ docs/openapi-fetch/api.md | 1 + packages/openapi-fetch/src/index.d.ts | 4 ++ packages/openapi-fetch/src/index.js | 19 +++++++-- packages/openapi-fetch/test/index.test.ts | 50 +++++++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 .changeset/happy-singers-fry.md diff --git a/.changeset/happy-singers-fry.md b/.changeset/happy-singers-fry.md new file mode 100644 index 000000000..16d22fb3b --- /dev/null +++ b/.changeset/happy-singers-fry.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": minor +--- + +Allow specifying baseUrl per request diff --git a/docs/openapi-fetch/api.md b/docs/openapi-fetch/api.md index 542f632b0..3a120cb38 100644 --- a/docs/openapi-fetch/api.md +++ b/docs/openapi-fetch/api.md @@ -36,6 +36,7 @@ client.GET("/my-url", options); | `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) | | `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) | | `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | (optional) Parse the response using [a built-in instance method](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods) (default: `"json"`). `"stream"` skips parsing altogether and returns the raw stream. | +| `baseUrl` | `string` | Prefix the fetch URL with this option (e.g. "https://myapi.dev/v1/") | | `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) | | `middleware` | `Middleware[]` | [See docs](/openapi-fetch/middleware-auth) | | (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) | diff --git a/packages/openapi-fetch/src/index.d.ts b/packages/openapi-fetch/src/index.d.ts index 8a41ce1e5..c2e9ebda3 100644 --- a/packages/openapi-fetch/src/index.d.ts +++ b/packages/openapi-fetch/src/index.d.ts @@ -110,6 +110,7 @@ export type FetchResponse = export type RequestOptions = ParamsOption & RequestBodyOption & { + baseUrl?: string; querySerializer?: QuerySerializer | QuerySerializerOptions; bodySerializer?: BodySerializer; parseAs?: ParseAs; @@ -292,3 +293,6 @@ export declare function createFinalURL( /** Merge headers a and b, with b taking priority */ export declare function mergeHeaders(...allHeaders: (HeadersOptions | undefined)[]): Headers; + +/** Remove trailing slash from url */ +export declare function removeTrailingSlash(url: string): string; diff --git a/packages/openapi-fetch/src/index.js b/packages/openapi-fetch/src/index.js index 22ca18e04..75fadaa0f 100644 --- a/packages/openapi-fetch/src/index.js +++ b/packages/openapi-fetch/src/index.js @@ -40,9 +40,7 @@ export default function createClient(clientOptions) { headers: baseHeaders, ...baseOptions } = { ...clientOptions }; - if (baseUrl.endsWith("/")) { - baseUrl = baseUrl.substring(0, baseUrl.length - 1); - } + baseUrl = removeTrailingSlash(baseUrl); baseHeaders = mergeHeaders(DEFAULT_HEADERS, baseHeaders); const middlewares = []; @@ -53,6 +51,7 @@ export default function createClient(clientOptions) { */ async function coreFetch(schemaPath, fetchOptions) { const { + baseUrl: localBaseUrl, fetch = baseFetch, headers, params = {}, @@ -61,6 +60,9 @@ export default function createClient(clientOptions) { bodySerializer = globalBodySerializer ?? defaultBodySerializer, ...init } = fetchOptions || {}; + if (localBaseUrl) { + baseUrl = removeTrailingSlash(localBaseUrl); + } let querySerializer = typeof globalQuerySerializer === "function" @@ -563,3 +565,14 @@ export function mergeHeaders(...allHeaders) { } return finalHeaders; } + +/** + * Remove trailing slash from url + * @type {import("./index.js").removeTrailingSlash} + */ +export function removeTrailingSlash(url) { + if (url.endsWith("/")) { + return url.substring(0, url.length - 1); + } + return url; +} diff --git a/packages/openapi-fetch/test/index.test.ts b/packages/openapi-fetch/test/index.test.ts index 2647fb388..14c7a9438 100644 --- a/packages/openapi-fetch/test/index.test.ts +++ b/packages/openapi-fetch/test/index.test.ts @@ -777,6 +777,29 @@ describe("client", () => { expect(getRequestUrl().href).toBe(toAbsoluteURL("/self")); }); + it("baseUrl per request", async () => { + const localBaseUrl = "https://api.foo.bar/v1"; + let client = createClient({ baseUrl }); + + const { getRequestUrl } = useMockRequestHandler({ + baseUrl: localBaseUrl, + method: "get", + path: "/self", + status: 200, + body: { message: "OK" }, + }); + + await client.GET("/self", { baseUrl: localBaseUrl }); + + // assert baseUrl and path mesh as expected + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self", localBaseUrl)); + + client = createClient({ baseUrl }); + await client.GET("/self", { baseUrl: localBaseUrl }); + // assert trailing '/' was removed + expect(getRequestUrl().href).toBe(toAbsoluteURL("/self", localBaseUrl)); + }); + describe("headers", () => { it("persist", async () => { const headers: HeadersInit = { Authorization: "Bearer secrettoken" }; @@ -1282,6 +1305,33 @@ describe("client", () => { expect(req.headers.get("onFetch")).toBe("exists"); expect(req.headers.get("onRequest")).toBe("exists"); }); + + it("baseUrl can be overridden", async () => { + useMockRequestHandler({ + baseUrl: "https://api.foo.bar/v1/", + method: "get", + path: "/self", + status: 200, + body: {}, + }); + + let requestBaseUrl = ""; + + const client = createClient({ + baseUrl, + }); + client.use({ + onRequest({ options }) { + requestBaseUrl = options.baseUrl; + return undefined; + }, + }); + + await client.GET("/self", { + baseUrl: "https://api.foo.bar/v1/", + }); + expect(requestBaseUrl).toBe("https://api.foo.bar/v1"); + }); }); });