Skip to content

Commit

Permalink
Add headers optional parameter to createClient and remove typecheckin…
Browse files Browse the repository at this point in the history
…g on auth if there is a Authorization header (#852)

Co-authored-by: Arda TANRIKULU <[email protected]>
  • Loading branch information
thelinuxlich and ardatan authored Dec 8, 2023
1 parent 719d1fc commit 40ff0b4
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-sheep-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fets': patch
---

Make auth params optional if they are provided in the client options as `globalParams`
75 changes: 73 additions & 2 deletions packages/fets/src/client/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { stringify as qsStringify, type IStringifyOptions } from 'qs';
import { fetch, FormData } from '@whatwg-node/fetch';
import { HTTPMethod } from '../typed-fetch.js';
import { OpenAPIDocument, Router } from '../types.js';
import { OpenAPIDocument, Router, SecurityScheme } from '../types.js';
import { createClientTypedResponsePromise } from './clientResponse.js';
import {
ClientMethod,
Expand All @@ -10,6 +10,7 @@ import {
ClientPlugin,
ClientRequestParams,
OASClient,
OASSecurityParams,
OnFetchHook,
OnRequestInitHook,
OnResponseHook,
Expand Down Expand Up @@ -50,6 +51,33 @@ function useValidationErrors(): ClientPlugin {

const EMPTY_OBJECT = {};

/**
* Create a client for an OpenAPI document
* You need to pass the imported OpenAPI document as a generic
*
* We recommend using the `NormalizeOAS` type to normalize the OpenAPI document
*
* @see https://the-guild.dev/openapi/fets/client/quick-start#usage-with-existing-rest-api
*
* @example
* ```ts
* import { createClient, type NormalizeOAS } from 'fets';
* import type oas from './oas.ts';
*
* const client = createClient<NormalizeOAS<typeof oas>>({});
* ```
*/
export function createClient<
const TOAS extends OpenAPIDocument & {
components: { securitySchemes: Record<string, SecurityScheme> };
},
>(
options: Omit<ClientOptionsWithStrictEndpoint<TOAS>, 'globalParams'> & {
globalParams: OASSecurityParams<
TOAS['components']['securitySchemes'][keyof TOAS['components']['securitySchemes']]
>;
},
): OASClient<TOAS, false>;
/**
* Create a client for an OpenAPI document
* You need to pass the imported OpenAPI document as a generic
Expand Down Expand Up @@ -77,7 +105,12 @@ export function createClient<const TOAS extends OpenAPIDocument>(
export function createClient<const TRouter extends Router<any, any, any>>(
options: ClientOptions,
): TRouter['__client'];
export function createClient({ endpoint, fetchFn = fetch, plugins = [] }: ClientOptions) {
export function createClient({
endpoint,
fetchFn = fetch,
plugins = [],
globalParams,
}: ClientOptions) {
plugins.unshift(useValidationErrors());
const onRequestInitHooks: OnRequestInitHook[] = [];
const onFetchHooks: OnFetchHook[] = [];
Expand All @@ -98,6 +131,44 @@ export function createClient({ endpoint, fetchFn = fetch, plugins = [] }: Client
return new Proxy(EMPTY_OBJECT, {
get(_target, method: HTTPMethod): ClientMethod {
async function clientMethod(requestParams: ClientRequestParams = {}) {
// Merge globalParams with the current requestParams
if (globalParams?.headers) {
requestParams.headers = {
...globalParams.headers,
...requestParams.headers,
};
}
if (globalParams?.query) {
requestParams.query = {
...globalParams.query,
...requestParams.query,
};
}
if (globalParams?.params) {
requestParams.params = {
...globalParams.params,
...requestParams.params,
};
}
if (globalParams?.json) {
requestParams.json = {
...globalParams.json,
...requestParams.json,
};
}
if (globalParams?.formData) {
requestParams.formData = {
...globalParams.formData,
...requestParams.formData,
};
}
if (globalParams?.formUrlEncoded) {
requestParams.formUrlEncoded = {
...globalParams.formUrlEncoded,
...requestParams.formUrlEncoded,
};
}

const {
headers = {},
params: paramsBody,
Expand Down
30 changes: 24 additions & 6 deletions packages/fets/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,20 @@ export type OASParamMap<TParameters extends { name: string; in: string }[]> = Pi
[Tuples.Map<OASParamToRequestParam<TParameters>>, Tuples.ToIntersection]
>;

export type OASClient<TOAS extends OpenAPIDocument> = {
export type OASClient<TOAS extends OpenAPIDocument, TAuthParamsRequired extends boolean = true> = {
/**
* The path to be used for the request
*/
[TPath in keyof OASPathMap<TOAS>]: {
/**
* HTTP Method to be used for this request
*/
[TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<TOAS, TPath, TMethod> extends
[TMethod in keyof OASMethodMap<TOAS, TPath>]: OASRequestParams<
TOAS,
TPath,
TMethod,
TAuthParamsRequired
> extends
| {
json: {};
}
Expand All @@ -205,10 +210,12 @@ export type OASClient<TOAS extends OpenAPIDocument> = {
query: {};
}
? (
requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod>> & ClientRequestInit,
requestParams: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> &
ClientRequestInit,
) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>>
: (
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod>> & ClientRequestInit,
requestParams?: Simplify<OASRequestParams<TOAS, TPath, TMethod, TAuthParamsRequired>> &
ClientRequestInit,
) => ClientTypedResponsePromise<OASResponse<TOAS, TPath, TMethod>>;
};
} & OASOAuthPath<TOAS>;
Expand Down Expand Up @@ -275,6 +282,7 @@ export type OASRequestParams<
TOAS extends OpenAPIDocument,
TPath extends keyof OASPathMap<TOAS>,
TMethod extends keyof OASMethodMap<TOAS, TPath>,
TAuthParamsRequired extends boolean = true,
> = (OASMethodMap<TOAS, TPath>[TMethod] extends {
requestBody: { content: { 'application/json': { schema: JSONSchema } } };
}
Expand Down Expand Up @@ -397,9 +405,15 @@ export type OASRequestParams<
}
: {}) &
// Respect security definitions in path object
OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]> &
(TAuthParamsRequired extends true
? OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]>
: DeepPartial<OASSecurityParamsBySecurityRef<TOAS, OASMethodMap<TOAS, TPath>[TMethod]>>) &
// Respect global security definitions
OASSecurityParamsBySecurityRef<TOAS, TOAS>;
(TAuthParamsRequired extends true
? OASSecurityParamsBySecurityRef<TOAS, TOAS>
: DeepPartial<OASSecurityParamsBySecurityRef<TOAS, TOAS>>);

type DeepPartial<T> = T extends Record<string, any> ? { [K in keyof T]?: DeepPartial<T[K]> } : T;

export type OASInput<
TOAS extends OpenAPIDocument,
Expand Down Expand Up @@ -442,6 +456,10 @@ export interface ClientOptions {
* @see https://the-guild.dev/openapi/fets/client/plugins
*/
plugins?: ClientPlugin[];
/**
* Global parameters
*/
globalParams?: ClientRequestParams;
}

export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit<
Expand Down
13 changes: 12 additions & 1 deletion packages/fets/tests/client/apiKey-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createClient, type NormalizeOAS } from '../../src';
import type apiKeyExampleOas from './fixtures/example-apiKey-header-oas';

const client = createClient<NormalizeOAS<typeof apiKeyExampleOas>>({});
type NormalizedOAS = NormalizeOAS<typeof apiKeyExampleOas>;
const client = createClient<NormalizedOAS>({});

const res = await client['/me'].get({
headers: {
Expand All @@ -15,3 +16,13 @@ if (!res.ok) {
}
const data = await res.json();
console.info(`User ${data.id}: ${data.name}`);

const clientWithPredefined = createClient<NormalizedOAS>({
globalParams: {
headers: {
'x-api-key': '123',
},
},
});

const res2 = await clientWithPredefined['/me'].get();

Check failure on line 28 in packages/fets/tests/client/apiKey-test.ts

View workflow job for this annotation

GitHub Actions / type-check

'res2' is declared but its value is never read.
33 changes: 33 additions & 0 deletions packages/fets/tests/client/global-params.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createClient, createRouter, Response } from 'fets';

describe('Client Global Params', () => {
it('should pass global params', async () => {
const router = createRouter().route({
path: '/test',
method: 'GET',
handler: req =>
Response.json({
headers: Object.fromEntries(req.headers.entries()),
query: req.query,
}),
});
const client = createClient<typeof router>({
fetchFn: router.fetch,
globalParams: {
headers: {
'x-api-key': '123',
},
query: {
foo: 'bar',
},
},
});

const res = await client['/test'].get();

expect(res.status).toBe(200);
const data = await res.json();
expect(data.headers['x-api-key']).toBe('123');
expect(data.query['foo']).toBe('bar');
});
});
17 changes: 17 additions & 0 deletions website/src/pages/client/client-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,20 @@ const client = createClient<typeof oas>({
fetch: fetchH2 as typeof fetch
})
```

## Global Parameters

You can also pass global parameters to the `createClient` function. These parameters will be passed
to every request made by the client.

```ts
import { oas } from './oas'

const client = createClient<typeof oas>({
globalParams: {
headers: {
Authorization: 'Bearer 123'
}
}
})
```
35 changes: 0 additions & 35 deletions website/src/pages/client/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,6 @@ A plugin in feTS is a function that returns an object with the following optiona
- It enables you to modify the response or throw an error to prevent the response from being
returned.

### Custom Plugin example

```ts
import { ClientPlugin } from 'fets'

export function myCustomPlugin(): ClientPlugin {
return {
onRequestInit({ requestInit }) {
requestInit.headers = {
...requestInit.headers,
'X-Custom-Header': 'Custom value'
}
},
onFetch({ fetchFn, setFetchFn }) {
setFetchFn(async (input, init) => {
const response = await fetchFn(input, init)
if (response.status === 401) {
throw new Error('Unauthorized')
}
return response
})
},
onResponse({ response }) {
if (response.status === 401) {
throw new Error('Unauthorized')
}
}
}
}

const client = createClient<typeof someoas>({
plugins: [myCustomPlugin()]
})
```

## Handling Cookies With Built-in Cookies Plugin

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

0 comments on commit 40ff0b4

Please sign in to comment.