diff --git a/.changeset/curvy-sheep-report.md b/.changeset/curvy-sheep-report.md new file mode 100644 index 000000000..99f8ee0b0 --- /dev/null +++ b/.changeset/curvy-sheep-report.md @@ -0,0 +1,5 @@ +--- +'fets': patch +--- + +Make auth params optional if they are provided in the client options as `globalParams` diff --git a/packages/fets/src/client/createClient.ts b/packages/fets/src/client/createClient.ts index b80829b62..65615c5ed 100644 --- a/packages/fets/src/client/createClient.ts +++ b/packages/fets/src/client/createClient.ts @@ -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, @@ -10,6 +10,7 @@ import { ClientPlugin, ClientRequestParams, OASClient, + OASSecurityParams, OnFetchHook, OnRequestInitHook, OnResponseHook, @@ -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>({}); + * ``` + */ +export function createClient< + const TOAS extends OpenAPIDocument & { + components: { securitySchemes: Record }; + }, +>( + options: Omit, 'globalParams'> & { + globalParams: OASSecurityParams< + TOAS['components']['securitySchemes'][keyof TOAS['components']['securitySchemes']] + >; + }, +): OASClient; /** * Create a client for an OpenAPI document * You need to pass the imported OpenAPI document as a generic @@ -77,7 +105,12 @@ export function createClient( export function createClient>( 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[] = []; @@ -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, diff --git a/packages/fets/src/client/types.ts b/packages/fets/src/client/types.ts index 6e881bb4e..5f783eb9b 100644 --- a/packages/fets/src/client/types.ts +++ b/packages/fets/src/client/types.ts @@ -183,7 +183,7 @@ export type OASParamMap = Pi [Tuples.Map>, Tuples.ToIntersection] >; -export type OASClient = { +export type OASClient = { /** * The path to be used for the request */ @@ -191,7 +191,12 @@ export type OASClient = { /** * HTTP Method to be used for this request */ - [TMethod in keyof OASMethodMap]: OASRequestParams extends + [TMethod in keyof OASMethodMap]: OASRequestParams< + TOAS, + TPath, + TMethod, + TAuthParamsRequired + > extends | { json: {}; } @@ -205,10 +210,12 @@ export type OASClient = { query: {}; } ? ( - requestParams: Simplify> & ClientRequestInit, + requestParams: Simplify> & + ClientRequestInit, ) => ClientTypedResponsePromise> : ( - requestParams?: Simplify> & ClientRequestInit, + requestParams?: Simplify> & + ClientRequestInit, ) => ClientTypedResponsePromise>; }; } & OASOAuthPath; @@ -275,6 +282,7 @@ export type OASRequestParams< TOAS extends OpenAPIDocument, TPath extends keyof OASPathMap, TMethod extends keyof OASMethodMap, + TAuthParamsRequired extends boolean = true, > = (OASMethodMap[TMethod] extends { requestBody: { content: { 'application/json': { schema: JSONSchema } } }; } @@ -397,9 +405,15 @@ export type OASRequestParams< } : {}) & // Respect security definitions in path object - OASSecurityParamsBySecurityRef[TMethod]> & + (TAuthParamsRequired extends true + ? OASSecurityParamsBySecurityRef[TMethod]> + : DeepPartial[TMethod]>>) & // Respect global security definitions - OASSecurityParamsBySecurityRef; + (TAuthParamsRequired extends true + ? OASSecurityParamsBySecurityRef + : DeepPartial>); + +type DeepPartial = T extends Record ? { [K in keyof T]?: DeepPartial } : T; export type OASInput< TOAS extends OpenAPIDocument, @@ -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 = Omit< diff --git a/packages/fets/tests/client/apiKey-test.ts b/packages/fets/tests/client/apiKey-test.ts index a37a18d5b..b314629a4 100644 --- a/packages/fets/tests/client/apiKey-test.ts +++ b/packages/fets/tests/client/apiKey-test.ts @@ -1,7 +1,8 @@ import { createClient, type NormalizeOAS } from '../../src'; import type apiKeyExampleOas from './fixtures/example-apiKey-header-oas'; -const client = createClient>({}); +type NormalizedOAS = NormalizeOAS; +const client = createClient({}); const res = await client['/me'].get({ headers: { @@ -15,3 +16,13 @@ if (!res.ok) { } const data = await res.json(); console.info(`User ${data.id}: ${data.name}`); + +const clientWithPredefined = createClient({ + globalParams: { + headers: { + 'x-api-key': '123', + }, + }, +}); + +const res2 = await clientWithPredefined['/me'].get(); diff --git a/packages/fets/tests/client/global-params.spec.ts b/packages/fets/tests/client/global-params.spec.ts new file mode 100644 index 000000000..95908ce5a --- /dev/null +++ b/packages/fets/tests/client/global-params.spec.ts @@ -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({ + 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'); + }); +}); diff --git a/website/src/pages/client/client-configuration.mdx b/website/src/pages/client/client-configuration.mdx index 3b804b045..96ebc5b91 100644 --- a/website/src/pages/client/client-configuration.mdx +++ b/website/src/pages/client/client-configuration.mdx @@ -21,3 +21,20 @@ const client = createClient({ 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({ + globalParams: { + headers: { + Authorization: 'Bearer 123' + } + } +}) +``` diff --git a/website/src/pages/client/plugins.mdx b/website/src/pages/client/plugins.mdx index 147202aef..fdf2cbd8f 100644 --- a/website/src/pages/client/plugins.mdx +++ b/website/src/pages/client/plugins.mdx @@ -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({ - plugins: [myCustomPlugin()] -}) -``` - ## Handling Cookies With Built-in Cookies Plugin To handle cookies separately from the environment, you can use the `useCookieStore` plugin.