From f878cd30f330557e1e67f86a4ae460e00485c7a7 Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Sat, 29 Apr 2023 22:33:47 -0600 Subject: [PATCH] support optional querySerializer option (#36) * support option querySerializer option * add tests * update readme * nicer test and example --------- Co-authored-by: Emory Petermann --- .prettierignore | 1 + README.md | 20 ++++++++++++++++++++ src/index.test.ts | 21 +++++++++++++++++++++ src/index.ts | 28 ++++++++++++---------------- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/.prettierignore b/.prettierignore index a095e95..d16f22a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ test/v1.d.ts +pnpm-lock.yaml diff --git a/README.md b/README.md index ea0cbfe..d7f94eb 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,26 @@ const { data, error } = await post('/create-post', { Note in the `get()` example, the URL was actually `/post/{post_id}`, _not_ `/post/my-post`. The URL matched the OpenAPI schema definition rather than the final URL. This library will replace the path param correctly for you, automatically. +### Query Parameters + +To customise the query parameters serialization pass in a `querySerializer` function to any fetch +method (get, post, etc): + +```ts +import createClient from 'openapi-fetch'; +import { paths } from './v1'; + +const { get, post } = createClient(); + +const { data, error } = await get('/post/{post_id}', { + params: { + path: { post_id: 'my-post' }, + query: { version: 2 }, + }, + querySerializer: (q) => `v=${q.version}`, +}); +``` + ### 🔒 Handling Auth Authentication often requires some reactivity dependent on a token. Since this library is so low-level, there are myriad ways to handle it: diff --git a/src/index.test.ts b/src/index.test.ts index ffa8001..ed3fbfe 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -172,6 +172,27 @@ describe('get()', () => { // expect post_id to be encoded properly expect(fetchMocker.mock.calls[0][0]).toBe('/post/post%3Fid%20%3D%20%F0%9F%A5%B4'); }); + + it('serializes params properly', async () => { + const client = createClient(); + fetchMocker.mockResponseOnce(() => ({ status: 200, body: '{}' })); + await client.get('/post/{post_id}', { + params: { path: { post_id: 'my-post' }, query: { a: 1, b: 2 } }, + }); + + expect(fetchMocker.mock.calls[0][0]).toBe('/post/my-post?a=1&b=2'); + }); + + it('serializes params properly with querySerializer', async () => { + const client = createClient(); + fetchMocker.mockResponseOnce(() => ({ status: 200, body: '{}' })); + await client.get('/post/{post_id}', { + params: { path: { post_id: 'my-post' }, query: { a: 1, b: 2 } }, + querySerializer: (q) => `alpha=${q.a}&beta=${q.b}`, + }); + + expect(fetchMocker.mock.calls[0][0]).toBe('/post/my-post?alpha=1&beta=2'); + }); }); describe('post()', () => { diff --git a/src/index.ts b/src/index.ts index c3785de..1d7d9d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,14 +21,10 @@ type PathsWith = { }[keyof T]; type PathParams = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams }; -type MethodParams = T extends { - parameters: any; -} - ? { params: T['parameters'] } - : { params?: BaseParams }; +type MethodParams = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams }; type Params = PathParams & MethodParams; type RequestBody = T extends { requestBody: any } ? { body: Unwrap } : { body?: never }; -type FetchOptions = Params & RequestBody & Omit; +type FetchOptions = Params & RequestBody & Omit & { querySerializer?: (query: any) => string }; type TruncatedResponse = Omit; /** Infer request/response from content type */ @@ -105,13 +101,13 @@ export default function createClient(options?: ClientOptions) { }); async function coreFetch(url: U, fetchOptions: FetchOptions): Promise> { - let { headers, body, params = {}, ...init } = fetchOptions || {}; + let { headers, body, params = {}, querySerializer = (q: any) => new URLSearchParams(q).toString(), ...init } = fetchOptions || {}; // URL let finalURL = `${options?.baseUrl ?? ''}${url as string}`; const { path, query } = (params as BaseParams | undefined) ?? {}; if (path) for (const [k, v] of Object.entries(path)) finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(`${v}`.trim())); - if (query) finalURL = `${finalURL}?${new URLSearchParams(query as any).toString()}`; + if (query) finalURL = `${finalURL}?${querySerializer(query as any)}`; // headers const baseHeaders = new Headers(defaultHeaders); // clone defaults (don’t overwrite!) @@ -145,35 +141,35 @@ export default function createClient(options?: ClientOptions) { return { /** Call a GET endpoint */ async get, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'GET' }); + return coreFetch(url, { ...init, method: 'GET' } as any); }, /** Call a PUT endpoint */ async put, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'PUT' }); + return coreFetch(url, { ...init, method: 'PUT' } as any); }, /** Call a POST endpoint */ async post, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'POST' }); + return coreFetch(url, { ...init, method: 'POST' } as any); }, /** Call a DELETE endpoint */ async del, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'DELETE' }); + return coreFetch(url, { ...init, method: 'DELETE' } as any); }, /** Call a OPTIONS endpoint */ async options, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'OPTIONS' }); + return coreFetch(url, { ...init, method: 'OPTIONS' } as any); }, /** Call a HEAD endpoint */ async head, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'HEAD' }); + return coreFetch(url, { ...init, method: 'HEAD' } as any); }, /** Call a PATCH endpoint */ async patch, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'PATCH' }); + return coreFetch(url, { ...init, method: 'PATCH' } as any); }, /** Call a TRACE endpoint */ async trace, M extends keyof T[U]>(url: U, init: FetchOptions) { - return coreFetch(url, { ...init, method: 'TRACE' }); + return coreFetch(url, { ...init, method: 'TRACE' } as any); }, }; }