Skip to content
This repository has been archived by the owner on May 22, 2023. It is now read-only.

Commit

Permalink
support optional querySerializer option (#36)
Browse files Browse the repository at this point in the history
* support option querySerializer option

* add tests

* update readme

* nicer test and example

---------

Co-authored-by: Emory Petermann <emory@onlyfor.us>
  • Loading branch information
drwpow and ezpuzz authored Apr 30, 2023
1 parent bd764b6 commit f878cd3
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 16 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
test/v1.d.ts
pnpm-lock.yaml
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<paths>();

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:
Expand Down
21 changes: 21 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<paths>();
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<paths>();
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()', () => {
Expand Down
28 changes: 12 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@ type PathsWith<T, M extends Method> = {
}[keyof T];

type PathParams<T> = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams };
type MethodParams<T> = T extends {
parameters: any;
}
? { params: T['parameters'] }
: { params?: BaseParams };
type MethodParams<T> = T extends { parameters: any } ? { params: T['parameters'] } : { params?: BaseParams };
type Params<T> = PathParams<T> & MethodParams<T>;
type RequestBody<T> = T extends { requestBody: any } ? { body: Unwrap<T['requestBody']> } : { body?: never };
type FetchOptions<T> = Params<T> & RequestBody<T> & Omit<RequestInit, 'body'>;
type FetchOptions<T> = Params<T> & RequestBody<T> & Omit<RequestInit, 'body'> & { querySerializer?: (query: any) => string };

type TruncatedResponse = Omit<Response, 'arrayBuffer' | 'blob' | 'body' | 'clone' | 'formData' | 'json' | 'text'>;
/** Infer request/response from content type */
Expand Down Expand Up @@ -105,13 +101,13 @@ export default function createClient<T>(options?: ClientOptions) {
});

async function coreFetch<U extends keyof T, M extends keyof T[U]>(url: U, fetchOptions: FetchOptions<T[U][M]>): Promise<FetchResponse<T[U][M]>> {
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!)
Expand Down Expand Up @@ -145,35 +141,35 @@ export default function createClient<T>(options?: ClientOptions) {
return {
/** Call a GET endpoint */
async get<U extends PathsWith<T, 'get'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'GET' });
return coreFetch(url, { ...init, method: 'GET' } as any);
},
/** Call a PUT endpoint */
async put<U extends PathsWith<T, 'put'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'PUT' });
return coreFetch(url, { ...init, method: 'PUT' } as any);
},
/** Call a POST endpoint */
async post<U extends PathsWith<T, 'post'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'POST' });
return coreFetch(url, { ...init, method: 'POST' } as any);
},
/** Call a DELETE endpoint */
async del<U extends PathsWith<T, 'delete'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'DELETE' });
return coreFetch(url, { ...init, method: 'DELETE' } as any);
},
/** Call a OPTIONS endpoint */
async options<U extends PathsWith<T, 'options'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'OPTIONS' });
return coreFetch(url, { ...init, method: 'OPTIONS' } as any);
},
/** Call a HEAD endpoint */
async head<U extends PathsWith<T, 'head'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'HEAD' });
return coreFetch(url, { ...init, method: 'HEAD' } as any);
},
/** Call a PATCH endpoint */
async patch<U extends PathsWith<T, 'patch'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'PATCH' });
return coreFetch(url, { ...init, method: 'PATCH' } as any);
},
/** Call a TRACE endpoint */
async trace<U extends PathsWith<T, 'trace'>, M extends keyof T[U]>(url: U, init: FetchOptions<T[U][M]>) {
return coreFetch(url, { ...init, method: 'TRACE' });
return coreFetch(url, { ...init, method: 'TRACE' } as any);
},
};
}

0 comments on commit f878cd3

Please sign in to comment.