fork of hey-api/openapi-ts because I need some additional things

chore: clean up fetch client API

Lubos b213fb5c 5d522f81

+216 -163
+16 -2
examples/openapi-ts-fetch/src/App.tsx
··· 15 15 baseUrl: 'https://api.fake-museum-example.com/v1.1', 16 16 }); 17 17 18 + const localClient = createClient({ 19 + baseUrl: 'https://api.fake-museum-example.com/v2', 20 + global: false, 21 + }); 22 + 23 + localClient.interceptors.request.use((request, options) => { 24 + console.log(options); 25 + return request; 26 + }); 27 + 18 28 function App() { 19 29 // const [pet, setPet] = useState<Awaited<ReturnType<typeof getPetById>>['data']>(); 20 30 21 31 const onGetSpecialEvent = async () => { 22 32 const { data, error } = await getSpecialEvent({ 33 + client: localClient, 23 34 path: { 24 35 eventId: 'dad4bce8-f5cb-4078-a211-995864315e39', 25 36 }, ··· 32 43 }; 33 44 34 45 const onBuyMuseumTickets = () => { 35 - // @ts-ignore 36 46 buyMuseumTickets({ 37 - body: '', 47 + body: { 48 + email: '', 49 + ticketDate: '', 50 + ticketType: 'event', 51 + }, 38 52 }); 39 53 }; 40 54
+31 -11
examples/openapi-ts-fetch/src/client/services.gen.ts
··· 34 34 * Get upcoming museum operating hours. 35 35 */ 36 36 export const getMuseumHours = (options?: Options<GetMuseumHoursData>) => 37 - client.get<GetMuseumHoursResponse2, GetMuseumHoursError>({ 38 - ...options, 39 - url: '/museum-hours', 40 - }); 37 + (options?.client ?? client).get<GetMuseumHoursResponse2, GetMuseumHoursError>( 38 + { 39 + ...options, 40 + url: '/museum-hours', 41 + }, 42 + ); 41 43 42 44 /** 43 45 * Create special events 44 46 * Creates a new special event for the museum. 45 47 */ 46 48 export const createSpecialEvent = (options: Options<CreateSpecialEventData>) => 47 - client.post<CreateSpecialEventResponse, CreateSpecialEventError>({ 49 + (options?.client ?? client).post< 50 + CreateSpecialEventResponse, 51 + CreateSpecialEventError 52 + >({ 48 53 ...options, 49 54 url: '/special-events', 50 55 }); ··· 54 59 * Return a list of upcoming special events at the museum. 55 60 */ 56 61 export const listSpecialEvents = (options?: Options<ListSpecialEventsData>) => 57 - client.get<ListSpecialEventsResponse2, ListSpecialEventsError>({ 62 + (options?.client ?? client).get< 63 + ListSpecialEventsResponse2, 64 + ListSpecialEventsError 65 + >({ 58 66 ...options, 59 67 url: '/special-events', 60 68 }); ··· 64 72 * Get details about a special event. 65 73 */ 66 74 export const getSpecialEvent = (options: Options<GetSpecialEventData>) => 67 - client.get<GetSpecialEventResponse, GetSpecialEventError>({ 75 + (options?.client ?? client).get< 76 + GetSpecialEventResponse, 77 + GetSpecialEventError 78 + >({ 68 79 ...options, 69 80 url: '/special-events/{eventId}', 70 81 }); ··· 74 85 * Update the details of a special event. 75 86 */ 76 87 export const updateSpecialEvent = (options: Options<UpdateSpecialEventData>) => 77 - client.patch<UpdateSpecialEventResponse, UpdateSpecialEventError>({ 88 + (options?.client ?? client).patch< 89 + UpdateSpecialEventResponse, 90 + UpdateSpecialEventError 91 + >({ 78 92 ...options, 79 93 url: '/special-events/{eventId}', 80 94 }); ··· 84 98 * Delete a special event from the collection. Allows museum to cancel planned events. 85 99 */ 86 100 export const deleteSpecialEvent = (options: Options<DeleteSpecialEventData>) => 87 - client.delete<DeleteSpecialEventResponse, DeleteSpecialEventError>({ 101 + (options?.client ?? client).delete< 102 + DeleteSpecialEventResponse, 103 + DeleteSpecialEventError 104 + >({ 88 105 ...options, 89 106 url: '/special-events/{eventId}', 90 107 }); ··· 94 111 * Purchase museum tickets for general entry or special events. 95 112 */ 96 113 export const buyMuseumTickets = (options: Options<BuyMuseumTicketsData>) => 97 - client.post<BuyMuseumTicketsResponse2, BuyMuseumTicketsError>({ 114 + (options?.client ?? client).post< 115 + BuyMuseumTicketsResponse2, 116 + BuyMuseumTicketsError 117 + >({ 98 118 ...options, 99 119 url: '/tickets', 100 120 }); ··· 104 124 * Return an image of your ticket with scannable QR code. Used for event entry. 105 125 */ 106 126 export const getTicketCode = (options: Options<GetTicketCodeData>) => 107 - client.get<GetTicketCodeResponse2, GetTicketCodeError>({ 127 + (options?.client ?? client).get<GetTicketCodeResponse2, GetTicketCodeError>({ 108 128 ...options, 109 129 url: '/tickets/{ticketId}/qr', 110 130 });
+31 -55
packages/client-fetch/src/index.ts
··· 1 - import type { Config, Req, RequestResult } from './types'; 1 + import type { Config, FetchClient, FinalRequestOptions } from './types'; 2 2 import { 3 3 createDefaultConfig, 4 4 createInterceptors, ··· 82 82 // } 83 83 // }; 84 84 85 - type Options = Omit<Req, 'method'>; 86 - 87 - type ReqInit = Omit<RequestInit, 'headers'> & { 85 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 86 + body?: any; 88 87 headers: ReturnType<typeof mergeHeaders>; 89 88 }; 90 - 91 - type Opts = Req & 92 - Config & { 93 - headers: ReturnType<typeof mergeHeaders>; 94 - }; 95 89 96 90 let globalConfig = createDefaultConfig(); 97 91 98 - const globalInterceptors = createInterceptors<Request, Response, Opts>(); 92 + const globalInterceptors = createInterceptors< 93 + Request, 94 + Response, 95 + FinalRequestOptions 96 + >(); 99 97 100 - export const createClient = (config: Partial<Config>) => { 98 + export const createClient = (config: Config): FetchClient => { 101 99 const defaultConfig = createDefaultConfig(); 102 100 const _config = { ...defaultConfig, ...config }; 103 101 104 - if (_config.baseUrl.endsWith('/')) { 102 + if (_config.baseUrl?.endsWith('/')) { 105 103 _config.baseUrl = _config.baseUrl.substring(0, _config.baseUrl.length - 1); 106 104 } 107 105 _config.headers = mergeHeaders(defaultConfig.headers, _config.headers); ··· 114 112 115 113 const interceptors = _config.global 116 114 ? globalInterceptors 117 - : createInterceptors<Request, Response, Opts>(); 115 + : createInterceptors<Request, Response, FinalRequestOptions>(); 118 116 119 - const request = async <Data = unknown, Error = unknown>( 120 - options: Req, 121 - ): RequestResult<Data, Error> => { 117 + // @ts-ignore 118 + const request: FetchClient['request'] = async (options) => { 122 119 const config = getConfig(); 123 120 124 - const opts: Opts = { 121 + const opts: FinalRequestOptions = { 125 122 ...config, 126 123 ...options, 127 124 headers: mergeHeaders(config.headers, options.headers), 128 125 }; 126 + if (opts.body && opts.bodySerializer) { 127 + opts.body = opts.bodySerializer(opts.body); 128 + } 129 129 130 130 const url = getUrl({ 131 - baseUrl: opts.baseUrl, 131 + baseUrl: opts.baseUrl ?? '', 132 132 path: opts.path, 133 133 query: opts.query, 134 134 querySerializer: ··· 142 142 redirect: 'follow', 143 143 ...opts, 144 144 }; 145 - if (opts.body) { 146 - requestInit.body = opts.bodySerializer(opts.body); 147 - } 148 145 // remove Content-Type if serialized body is FormData; browser will correctly set Content-Type and boundary expression 149 146 if (requestInit.body instanceof FormData) { 150 147 requestInit.headers.delete('Content-Type'); ··· 156 153 request = await fn(request, opts); 157 154 } 158 155 159 - const _fetch = opts.fetch; 156 + const _fetch = opts.fetch!; 160 157 let response = await _fetch(request); 161 158 162 159 for (const fn of interceptors.response._fns) { ··· 175 172 ) { 176 173 if (response.ok) { 177 174 return { 178 - // @ts-ignore 179 175 data: {}, 180 176 ...result, 181 177 }; 182 178 } 183 179 return { 184 - // @ts-ignore 185 180 error: {}, 186 181 ...result, 187 182 }; ··· 190 185 if (response.ok) { 191 186 if (opts.parseAs === 'stream') { 192 187 return { 193 - // @ts-ignore 194 188 data: response.body, 195 189 ...result, 196 190 }; 197 191 } 198 192 return { 199 - data: await response[opts.parseAs](), 193 + data: await response[opts.parseAs ?? 'json'](), 200 194 ...result, 201 195 }; 202 196 } ··· 208 202 // noop 209 203 } 210 204 return { 211 - // @ts-ignore 212 205 error, 213 206 ...result, 214 207 }; 215 208 }; 216 209 217 - type Interceptors = { 218 - [P in keyof typeof interceptors]: Pick< 219 - (typeof interceptors)[P], 220 - 'eject' | 'use' 221 - >; 222 - }; 223 - 224 - const client = { 225 - connect: <Data = unknown, Error = unknown>(options: Options) => 226 - request<Data, Error>({ ...options, method: 'CONNECT' }), 227 - delete: <Data = unknown, Error = unknown>(options: Options) => 228 - request<Data, Error>({ ...options, method: 'DELETE' }), 229 - get: <Data = unknown, Error = unknown>(options: Options) => 230 - request<Data, Error>({ ...options, method: 'GET' }), 210 + return { 211 + connect: (options) => request({ ...options, method: 'CONNECT' }), 212 + delete: (options) => request({ ...options, method: 'DELETE' }), 213 + get: (options) => request({ ...options, method: 'GET' }), 231 214 getConfig, 232 - head: <Data = unknown, Error = unknown>(options: Options) => 233 - request<Data, Error>({ ...options, method: 'HEAD' }), 234 - interceptors: interceptors as Interceptors, 235 - options: <Data = unknown, Error = unknown>(options: Options) => 236 - request<Data, Error>({ ...options, method: 'OPTIONS' }), 237 - patch: <Data = unknown, Error = unknown>(options: Options) => 238 - request<Data, Error>({ ...options, method: 'PATCH' }), 239 - post: <Data = unknown, Error = unknown>(options: Options) => 240 - request<Data, Error>({ ...options, method: 'POST' }), 241 - put: <Data = unknown, Error = unknown>(options: Options) => 242 - request<Data, Error>({ ...options, method: 'PUT' }), 215 + head: (options) => request({ ...options, method: 'HEAD' }), 216 + interceptors, 217 + options: (options) => request({ ...options, method: 'OPTIONS' }), 218 + patch: (options) => request({ ...options, method: 'PATCH' }), 219 + post: (options) => request({ ...options, method: 'POST' }), 220 + put: (options) => request({ ...options, method: 'PUT' }), 243 221 request, 244 - trace: <Data = unknown, Error = unknown>(options: Options) => 245 - request<Data, Error>({ ...options, method: 'TRACE' }), 222 + trace: (options) => request({ ...options, method: 'TRACE' }), 246 223 }; 247 - return client; 248 224 }; 249 225 250 226 export const client = createClient(globalConfig);
+111 -57
packages/client-fetch/src/types.ts
··· 1 1 import type { 2 2 BodySerializer, 3 - FetchOptions, 3 + Middleware, 4 4 QuerySerializer, 5 5 QuerySerializerOptions, 6 6 } from './utils'; 7 7 8 - type ApiRequestOptions = { 9 - readonly body?: any; 10 - readonly cookies?: Record<string, unknown>; 11 - readonly errors?: Record<number, string>; 12 - readonly formData?: Record<string, unknown>; 13 - readonly headers?: Record<string, unknown>; 14 - readonly mediaType?: string; 15 - readonly method: 8 + type OmitKey<T, K> = Pick<T, Exclude<keyof T, K>>; 9 + 10 + export interface Config 11 + extends Omit<RequestInit, 'body' | 'headers' | 'method'> { 12 + /** 13 + * Base URL for all requests made by this client. 14 + * @default '' 15 + */ 16 + baseUrl?: string; 17 + /** 18 + * Any body that you want to add to your request. 19 + * 20 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 21 + */ 22 + body?: RequestInit['body'] | Record<string, unknown>; 23 + /** 24 + * A function for serializing request body parameter. By default, 25 + * {@link JSON.stringify()} will be used. 26 + */ 27 + bodySerializer?: BodySerializer; 28 + /** 29 + * Fetch API implementation. You can use this option to provide a custom 30 + * fetch instance. 31 + * @default globalThis.fetch 32 + */ 33 + fetch?: (request: Request) => ReturnType<typeof fetch>; 34 + /** 35 + * By default, options passed to this call will be applied to the global 36 + * client instance. Set to false to create a local client instance. 37 + * @default true 38 + */ 39 + global?: boolean; 40 + /** 41 + * An object containing any HTTP headers that you want to pre-populate your 42 + * `Headers` object with. 43 + * 44 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 45 + */ 46 + headers?: 47 + | RequestInit['headers'] 48 + | Record< 49 + string, 50 + | string 51 + | number 52 + | boolean 53 + | (string | number | boolean)[] 54 + | null 55 + | undefined 56 + >; 57 + /** 58 + * The request method. 59 + * 60 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 61 + */ 62 + method?: 16 63 | 'CONNECT' 17 64 | 'DELETE' 18 65 | 'GET' ··· 22 69 | 'POST' 23 70 | 'PUT' 24 71 | 'TRACE'; 25 - readonly path?: Record<string, unknown>; 26 - readonly query?: Record<string, unknown>; 27 - readonly responseHeader?: string; 28 - readonly url: string; 29 - }; 30 - 31 - interface FetchConfig extends FetchOptions { 32 72 /** 33 - * custom fetch 34 - * @default globalThis.fetch 73 + * Return the response data parsed in a specified format. Any of the 74 + * {@link Body} methods are allowed. By default, {@link Body.json()} will be 75 + * used. Select `stream` if you don't want to parse response data. 76 + * @default 'json' 35 77 */ 36 - fetch: (request: Request) => ReturnType<typeof fetch>; 78 + parseAs?: Exclude<keyof Body, 'body' | 'bodyUsed'> | 'stream'; 79 + /** 80 + * A function for serializing request query parameters. By default, arrays 81 + * will be exploded in form style, objects will be exploded in deepObject 82 + * style, and reserved characters are percent-encoded. 83 + * 84 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 85 + */ 86 + querySerializer?: QuerySerializer | QuerySerializerOptions; 37 87 } 38 88 39 - interface BodyType<T = unknown> { 40 - arrayBuffer: Awaited<ReturnType<Response['arrayBuffer']>>; 41 - blob: Awaited<ReturnType<Response['blob']>>; 42 - json: T; 43 - stream: Response['body']; 44 - text: Awaited<ReturnType<Response['text']>>; 89 + interface RequestOptions extends Omit<Config, 'global'> { 90 + path?: Record<string, unknown>; 91 + query?: Record<string, unknown>; 92 + url: string; 45 93 } 46 94 47 - interface RequestResponse<Data = unknown, Error = unknown> { 95 + type RequestResult<Data = unknown, Error = unknown> = Promise<{ 48 96 error?: Error; 49 97 data?: Data; 50 98 request: Request; 51 99 response: Response; 100 + }>; 101 + 102 + type MethodFn = <Data = unknown, Error = unknown>( 103 + options: RequestOptions, 104 + ) => RequestResult<Data, Error>; 105 + type RequestFn = <Data = unknown, Error = unknown>( 106 + options: RequestOptions & Pick<Required<RequestOptions>, 'method'>, 107 + ) => RequestResult<Data, Error>; 108 + 109 + interface Client<Request = unknown, Response = unknown, Options = unknown> { 110 + connect: MethodFn; 111 + delete: MethodFn; 112 + get: MethodFn; 113 + getConfig: () => Config; 114 + head: MethodFn; 115 + interceptors: Middleware<Request, Response, Options>; 116 + options: MethodFn; 117 + patch: MethodFn; 118 + post: MethodFn; 119 + put: MethodFn; 120 + request: RequestFn; 121 + trace: MethodFn; 52 122 } 53 123 54 - export type RequestResult<Data = unknown, Error = unknown> = Promise< 55 - RequestResponse<Data, Error> 56 - >; 124 + export type FinalRequestOptions = RequestOptions & 125 + Config & { 126 + headers: Headers; 127 + }; 57 128 58 - export interface Config extends FetchConfig { 59 - /** 60 - * Base URL... 61 - * @default '' 62 - */ 63 - baseUrl: string; 64 - /** 65 - * Body serializer... 66 - */ 67 - bodySerializer: BodySerializer<unknown>; 68 - /** 69 - * Global?? 70 - * @default true 71 - */ 72 - global: boolean; 73 - /** 74 - * Parse as... 75 - * @default 'json' 76 - */ 77 - parseAs: keyof BodyType; 129 + export type FetchClient = Client<Request, Response, FinalRequestOptions>; 130 + 131 + type OptionsBase = Omit<RequestOptions, 'url'> & { 78 132 /** 79 - * Query serializer... 133 + * You can provide a client instance returned by `createClient()` instead of 134 + * individual options. This might be also useful if you want to implement a 135 + * custom client. 80 136 */ 81 - querySerializer: QuerySerializer<unknown> | QuerySerializerOptions; 82 - } 137 + client?: FetchClient; 138 + }; 83 139 84 - export interface Req 85 - extends Omit<ApiRequestOptions, 'headers'>, 86 - Omit<Partial<Config>, 'body' | 'method'> {} 87 - 88 - export type Options<T = unknown> = Omit<Req, 'method' | 'url'> & T; 140 + export type Options<T = unknown> = T extends { body: any } 141 + ? OmitKey<OptionsBase, 'body'> & T 142 + : OptionsBase & T;
+25 -36
packages/client-fetch/src/utils.ts
··· 13 13 type ObjectStyle = 'form' | 'deepObject'; 14 14 type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 15 15 16 - export interface FetchOptions extends Omit<RequestInit, 'headers'> { 17 - /** 18 - * Headers... 19 - */ 20 - headers?: HeadersOptions; 21 - } 16 + export type QuerySerializer = (query: Record<string, unknown>) => string; 22 17 23 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 - export type QuerySerializer<T = unknown> = ( 25 - query: Record<string, unknown>, 26 - ) => string; 27 - 28 - // type BodySerializer<T> = (body: OperationRequestBodyContent<T>) => any; 29 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 - export type BodySerializer<T> = (body: any) => any; 18 + export type BodySerializer = (body: any) => any; 31 19 32 20 interface SerializerOptions<T> { 33 21 /** ··· 48 36 value: string; 49 37 } 50 38 51 - export type HeadersOptions = 52 - | HeadersInit 53 - | Record< 54 - string, 55 - | string 56 - | number 57 - | boolean 58 - | (string | number | boolean)[] 59 - | null 60 - | undefined 61 - >; 62 - 63 39 export interface QuerySerializerOptions { 64 40 allowReserved?: boolean; 65 41 array?: SerializerOptions<ArrayStyle>; 66 42 object?: SerializerOptions<ObjectStyle>; 67 43 } 68 44 69 - export function serializePrimitiveParam({ 45 + function serializePrimitiveParam({ 70 46 allowReserved, 71 47 name, 72 48 value, ··· 123 99 } 124 100 }; 125 101 126 - export function serializeArrayParam({ 102 + function serializeArrayParam({ 127 103 allowReserved, 128 104 explode, 129 105 name, ··· 165 141 return style === 'label' || style === 'matrix' ? separator + final : final; 166 142 } 167 143 168 - export const serializeObjectParam = ({ 144 + const serializeObjectParam = ({ 169 145 allowReserved, 170 146 explode, 171 147 name, ··· 208 184 return style === 'label' || style === 'matrix' ? separator + final : final; 209 185 }; 210 186 211 - export function defaultPathSerializer({ path, url: _url }: PathSerializer) { 187 + function defaultPathSerializer({ path, url: _url }: PathSerializer) { 212 188 let url = _url; 213 189 const matches = _url.match(PATH_PARAM_RE); 214 190 if (matches) { ··· 362 338 return url; 363 339 } 364 340 365 - export const mergeHeaders = (...headers: Array<HeadersOptions | undefined>) => { 366 - const finalHeaders = new Headers(); 341 + export const mergeHeaders = ( 342 + ...headers: Array<Required<Config>['headers'] | undefined> 343 + ) => { 344 + const mergedHeaders = new Headers(); 367 345 for (const header of headers) { 368 346 if (!header || typeof header !== 'object') { 369 347 continue; ··· 374 352 375 353 for (const [key, value] of iterator) { 376 354 if (value === null) { 377 - finalHeaders.delete(key); 355 + mergedHeaders.delete(key); 378 356 } else if (Array.isArray(value)) { 379 357 for (const v of value) { 380 - finalHeaders.append(key, v as string); 358 + mergedHeaders.append(key, v as string); 381 359 } 382 360 } else if (value !== undefined) { 383 - finalHeaders.set(key, value as string); 361 + mergedHeaders.set(key, value as string); 384 362 } 385 363 } 386 364 } 387 - return finalHeaders; 365 + return mergedHeaders; 388 366 }; 389 367 390 368 type ReqInterceptor<Req, Options> = ( ··· 417 395 } 418 396 } 419 397 398 + // `createInterceptors()` response, meant for external use as it does not 399 + // expose internals 400 + export interface Middleware<Req, Res, Options> { 401 + request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>; 402 + response: Pick< 403 + Interceptors<ResInterceptor<Res, Req, Options>>, 404 + 'eject' | 'use' 405 + >; 406 + } 407 + 408 + // do not add `Middleware` as return type so we can use _fns internally 420 409 export const createInterceptors = <Req, Res, Options>() => ({ 421 410 request: new Interceptors<ReqInterceptor<Req, Options>>(), 422 411 response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
+1 -1
packages/openapi-ts/src/utils/enum.ts
··· 12 12 * 3: Add '_' where the string transitions from lowercase to uppercase 13 13 * 4: Transform the whole string to uppercase 14 14 * 15 - * Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers 15 + * Javascript identifier regexp pattern retrieved from https://developer.mozilla.org/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers 16 16 */ 17 17 export const enumKey = (value?: string | number, customName?: string) => { 18 18 if (customName) {
+1 -1
packages/openapi-ts/src/utils/write/services.ts
··· 331 331 return [ 332 332 compiler.return.functionCall({ 333 333 args: [options], 334 - name: `client.${operation.method.toLocaleLowerCase()}`, 334 + name: `(options?.client ?? client).${operation.method.toLocaleLowerCase()}`, 335 335 types: 336 336 errorType && responseType 337 337 ? [responseType, errorType]