Skip to content

Commit 40ff0b4

Browse files
Add headers optional parameter to createClient and remove typechecking on auth if there is a Authorization header (#852)
Co-authored-by: Arda TANRIKULU <[email protected]>
1 parent 719d1fc commit 40ff0b4

File tree

7 files changed

+164
-44
lines changed

7 files changed

+164
-44
lines changed

.changeset/curvy-sheep-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'fets': patch
3+
---
4+
5+
Make auth params optional if they are provided in the client options as `globalParams`

packages/fets/src/client/createClient.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { stringify as qsStringify, type IStringifyOptions } from 'qs';
22
import { fetch, FormData } from '@whatwg-node/fetch';
33
import { HTTPMethod } from '../typed-fetch.js';
4-
import { OpenAPIDocument, Router } from '../types.js';
4+
import { OpenAPIDocument, Router, SecurityScheme } from '../types.js';
55
import { createClientTypedResponsePromise } from './clientResponse.js';
66
import {
77
ClientMethod,
@@ -10,6 +10,7 @@ import {
1010
ClientPlugin,
1111
ClientRequestParams,
1212
OASClient,
13+
OASSecurityParams,
1314
OnFetchHook,
1415
OnRequestInitHook,
1516
OnResponseHook,
@@ -50,6 +51,33 @@ function useValidationErrors(): ClientPlugin {
5051

5152
const EMPTY_OBJECT = {};
5253

54+
/**
55+
* Create a client for an OpenAPI document
56+
* You need to pass the imported OpenAPI document as a generic
57+
*
58+
* We recommend using the `NormalizeOAS` type to normalize the OpenAPI document
59+
*
60+
* @see https://the-guild.dev/openapi/fets/client/quick-start#usage-with-existing-rest-api
61+
*
62+
* @example
63+
* ```ts
64+
* import { createClient, type NormalizeOAS } from 'fets';
65+
* import type oas from './oas.ts';
66+
*
67+
* const client = createClient<NormalizeOAS<typeof oas>>({});
68+
* ```
69+
*/
70+
export function createClient<
71+
const TOAS extends OpenAPIDocument & {
72+
components: { securitySchemes: Record<string, SecurityScheme> };
73+
},
74+
>(
75+
options: Omit<ClientOptionsWithStrictEndpoint<TOAS>, 'globalParams'> & {
76+
globalParams: OASSecurityParams<
77+
TOAS['components']['securitySchemes'][keyof TOAS['components']['securitySchemes']]
78+
>;
79+
},
80+
): OASClient<TOAS, false>;
5381
/**
5482
* Create a client for an OpenAPI document
5583
* You need to pass the imported OpenAPI document as a generic
@@ -77,7 +105,12 @@ export function createClient<const TOAS extends OpenAPIDocument>(
77105
export function createClient<const TRouter extends Router<any, any, any>>(
78106
options: ClientOptions,
79107
): TRouter['__client'];
80-
export function createClient({ endpoint, fetchFn = fetch, plugins = [] }: ClientOptions) {
108+
export function createClient({
109+
endpoint,
110+
fetchFn = fetch,
111+
plugins = [],
112+
globalParams,
113+
}: ClientOptions) {
81114
plugins.unshift(useValidationErrors());
82115
const onRequestInitHooks: OnRequestInitHook[] = [];
83116
const onFetchHooks: OnFetchHook[] = [];
@@ -98,6 +131,44 @@ export function createClient({ endpoint, fetchFn = fetch, plugins = [] }: Client
98131
return new Proxy(EMPTY_OBJECT, {
99132
get(_target, method: HTTPMethod): ClientMethod {
100133
async function clientMethod(requestParams: ClientRequestParams = {}) {
134+
// Merge globalParams with the current requestParams
135+
if (globalParams?.headers) {
136+
requestParams.headers = {
137+
...globalParams.headers,
138+
...requestParams.headers,
139+
};
140+
}
141+
if (globalParams?.query) {
142+
requestParams.query = {
143+
...globalParams.query,
144+
...requestParams.query,
145+
};
146+
}
147+
if (globalParams?.params) {
148+
requestParams.params = {
149+
...globalParams.params,
150+
...requestParams.params,
151+
};
152+
}
153+
if (globalParams?.json) {
154+
requestParams.json = {
155+
...globalParams.json,
156+
...requestParams.json,
157+
};
158+
}
159+
if (globalParams?.formData) {
160+
requestParams.formData = {
161+
...globalParams.formData,
162+
...requestParams.formData,
163+
};
164+
}
165+
if (globalParams?.formUrlEncoded) {
166+
requestParams.formUrlEncoded = {
167+
...globalParams.formUrlEncoded,
168+
...requestParams.formUrlEncoded,
169+
};
170+
}
171+
101172
const {
102173
headers = {},
103174
params: paramsBody,

packages/fets/src/client/types.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,20 @@ export type OASParamMap<TParameters extends { name: string; in: string }[]> = Pi
183183
[Tuples.Map<OASParamToRequestParam<TParameters>>, Tuples.ToIntersection]
184184
>;
185185

186-
export type OASClient<TOAS extends OpenAPIDocument> = {
186+
export type OASClient<TOAS extends OpenAPIDocument, TAuthParamsRequired extends boolean = true> = {
187187
/**
188188
* The path to be used for the request
189189
*/
190190
[TPath in keyof OASPathMap<TOAS>]: {
191191
/**
192192
* HTTP Method to be used for this request
193193
*/
194-
[TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<TOAS, TPath, TMethod> extends
194+
[TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<
195+
TOAS,
196+
TPath,
197+
TMethod,
198+
TAuthParamsRequired
199+
> extends
195200
| {
196201
json: {};
197202
}
@@ -205,10 +210,12 @@ export type OASClient<TOAS extends OpenAPIDocument> = {
205210
query: {};
206211
}
207212
? (
208-
requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod>> & ClientRequestInit,
213+
requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> &
214+
ClientRequestInit,
209215
) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>>
210216
: (
211-
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod>> & ClientRequestInit,
217+
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> &
218+
ClientRequestInit,
212219
) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>>;
213220
};
214221
} & OASOAuthPath<TOAS>;
@@ -275,6 +282,7 @@ export type OASRequestParams<
275282
TOAS extends OpenAPIDocument,
276283
TPath extends keyof OASPathMap<TOAS>,
277284
TMethod extends keyof OASMethodMap<TOAS, TPath>,
285+
TAuthParamsRequired extends boolean = true,
278286
> = (OASMethodMap<TOAS, TPath>[TMethod] extends {
279287
requestBody: { content: { 'application/json': { schema: JSONSchema } } };
280288
}
@@ -397,9 +405,15 @@ export type OASRequestParams<
397405
}
398406
: {}) &
399407
// Respect security definitions in path object
400-
OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]> &
408+
(TAuthParamsRequired extends true
409+
? OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]>
410+
: DeepPartial<OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]>>) &
401411
// Respect global security definitions
402-
OASSecurityParamsBySecurityRef<TOAS, TOAS>;
412+
(TAuthParamsRequired extends true
413+
? OASSecurityParamsBySecurityRef<TOAS, TOAS>
414+
: DeepPartial<OASSecurityParamsBySecurityRef<TOAS, TOAS>>);
415+
416+
type DeepPartial<T> = T extends Record<string, any> ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
403417

404418
export type OASInput<
405419
TOAS extends OpenAPIDocument,
@@ -442,6 +456,10 @@ export interface ClientOptions {
442456
* @see https://the-guild.dev/openapi/fets/client/plugins
443457
*/
444458
plugins?: ClientPlugin[];
459+
/**
460+
* Global parameters
461+
*/
462+
globalParams?: ClientRequestParams;
445463
}
446464

447465
export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit<

packages/fets/tests/client/apiKey-test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { createClient, type NormalizeOAS } from '../../src';
22
import type apiKeyExampleOas from './fixtures/example-apiKey-header-oas';
33

4-
const client = createClient<NormalizeOAS<typeof apiKeyExampleOas>>({});
4+
type NormalizedOAS = NormalizeOAS<typeof apiKeyExampleOas>;
5+
const client = createClient<NormalizedOAS>({});
56

67
const res = await client['/me'].get({
78
headers: {
@@ -15,3 +16,13 @@ if (!res.ok) {
1516
}
1617
const data = await res.json();
1718
console.info(`User ${data.id}: ${data.name}`);
19+
20+
const clientWithPredefined = createClient<NormalizedOAS>({
21+
globalParams: {
22+
headers: {
23+
'x-api-key': '123',
24+
},
25+
},
26+
});
27+
28+
const res2 = await clientWithPredefined['/me'].get();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient, createRouter, Response } from 'fets';
2+
3+
describe('Client Global Params', () => {
4+
it('should pass global params', async () => {
5+
const router = createRouter().route({
6+
path: '/test',
7+
method: 'GET',
8+
handler: req =>
9+
Response.json({
10+
headers: Object.fromEntries(req.headers.entries()),
11+
query: req.query,
12+
}),
13+
});
14+
const client = createClient<typeof router>({
15+
fetchFn: router.fetch,
16+
globalParams: {
17+
headers: {
18+
'x-api-key': '123',
19+
},
20+
query: {
21+
foo: 'bar',
22+
},
23+
},
24+
});
25+
26+
const res = await client['/test'].get();
27+
28+
expect(res.status).toBe(200);
29+
const data = await res.json();
30+
expect(data.headers['x-api-key']).toBe('123');
31+
expect(data.query['foo']).toBe('bar');
32+
});
33+
});

website/src/pages/client/client-configuration.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,20 @@ const client = createClient<typeof oas>({
2121
fetch: fetchH2 as typeof fetch
2222
})
2323
```
24+
25+
## Global Parameters
26+
27+
You can also pass global parameters to the `createClient` function. These parameters will be passed
28+
to every request made by the client.
29+
30+
```ts
31+
import { oas } from './oas'
32+
33+
const client = createClient<typeof oas>({
34+
globalParams: {
35+
headers: {
36+
Authorization: 'Bearer 123'
37+
}
38+
}
39+
})
40+
```

website/src/pages/client/plugins.mdx

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,6 @@ A plugin in feTS is a function that returns an object with the following optiona
1616
- It enables you to modify the response or throw an error to prevent the response from being
1717
returned.
1818

19-
### Custom Plugin example
20-
21-
```ts
22-
import { ClientPlugin } from 'fets'
23-
24-
export function myCustomPlugin(): ClientPlugin {
25-
return {
26-
onRequestInit({ requestInit }) {
27-
requestInit.headers = {
28-
...requestInit.headers,
29-
'X-Custom-Header': 'Custom value'
30-
}
31-
},
32-
onFetch({ fetchFn, setFetchFn }) {
33-
setFetchFn(async (input, init) => {
34-
const response = await fetchFn(input, init)
35-
if (response.status === 401) {
36-
throw new Error('Unauthorized')
37-
}
38-
return response
39-
})
40-
},
41-
onResponse({ response }) {
42-
if (response.status === 401) {
43-
throw new Error('Unauthorized')
44-
}
45-
}
46-
}
47-
}
48-
49-
const client = createClient<typeof someoas>({
50-
plugins: [myCustomPlugin()]
51-
})
52-
```
53-
5419
## Handling Cookies With Built-in Cookies Plugin
5520

5621
To handle cookies separately from the environment, you can use the `useCookieStore` plugin.

0 commit comments

Comments
 (0)