diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c50f87ce3..7868fa943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: - name: Build packages run: pnpm build --filter="@hey-api/**" + - name: Check examples generated code + if: matrix.node-version == '24.10.0' && matrix.os == 'ubuntu-latest' + run: pnpm examples:check + - name: Build examples if: matrix.node-version == '24.10.0' && matrix.os == 'ubuntu-latest' run: pnpm build --filter="@example/**" diff --git a/examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts b/examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts similarity index 53% rename from examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts rename to examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts index f4be36afb..e063c5925 100644 --- a/examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts @@ -1,9 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts -import { httpResource } from '@angular/common/http'; +import { type HttpRequest, httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import type { Options } from '../../../sdk.gen'; +import { client } from '../client.gen'; +import type { Options } from '../sdk.gen'; import type { AddPetData, AddPetResponse, @@ -38,28 +39,294 @@ import type { UpdateUserData, UploadFileData, UploadFileResponse, -} from '../../../types.gen'; -import { - addPetRequest, - createUserRequest, - createUsersWithListInputRequest, - deleteOrderRequest, - deletePetRequest, - deleteUserRequest, - findPetsByStatusRequest, - findPetsByTagsRequest, - getInventoryRequest, - getOrderByIdRequest, - getPetByIdRequest, - getUserByNameRequest, - loginUserRequest, - logoutUserRequest, - placeOrderRequest, - updatePetRequest, - updatePetWithFormRequest, - updateUserRequest, - uploadFileRequest, -} from './requests.gen'; +} from '../types.gen'; + +/** + * Add a new pet to the store. + * + * Add a new pet to the store. + */ +export const addPetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet', + ...options, + }); + +/** + * Update an existing pet. + * + * Update an existing pet by Id. + */ +export const updatePetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'PUT', + responseStyle: 'data', + url: '/pet', + ...options, + }); + +/** + * Finds Pets by status. + * + * Multiple status values can be provided with comma separated strings. + */ +export const findPetsByStatusRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/findByStatus', + ...options, + }); + +/** + * Finds Pets by tags. + * + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + */ +export const findPetsByTagsRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/findByTags', + ...options, + }); + +/** + * Deletes a pet. + * + * Delete a pet. + */ +export const deletePetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Find pet by ID. + * + * Returns a single pet. + */ +export const getPetByIdRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Updates a pet in the store with form data. + * + * Updates a pet resource based on the form data. + */ +export const updatePetWithFormRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Uploads an image. + * + * Upload image of the pet. + */ +export const uploadFileRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet/{petId}/uploadImage', + ...options, + }); + +/** + * Returns pet inventories by status. + * + * Returns a map of status codes to quantities. + */ +export const getInventoryRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/store/inventory', + ...options, + }); + +/** + * Place an order for a pet. + * + * Place a new order in the store. + */ +export const placeOrderRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/store/order', + ...options, + }); + +/** + * Delete purchase order by identifier. + * + * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. + */ +export const deleteOrderRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Find purchase order by ID. + * + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + */ +export const getOrderByIdRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Create user. + * + * This can only be done by the logged in user. + */ +export const createUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/user', + ...options, + }); + +/** + * Creates list of users with given input array. + * + * Creates list of users with given input array. + */ +export const createUsersWithListInputRequest = < + ThrowOnError extends boolean = false, +>( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/user/createWithList', + ...options, + }); + +/** + * Logs user into the system. + * + * Log into the system. + */ +export const loginUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/login', + ...options, + }); + +/** + * Logs out current logged in user session. + * + * Log user out of the system. + */ +export const logoutUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/logout', + ...options, + }); + +/** + * Delete user resource. + * + * This can only be done by the logged in user. + */ +export const deleteUserRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); + +/** + * Get user by user name. + * + * Get user detail based on username. + */ +export const getUserByNameRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); + +/** + * Update user resource. + * + * This can only be done by the logged in user. + */ +export const updateUserRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'PUT', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); @Injectable({ providedIn: 'root', @@ -67,6 +334,7 @@ import { export class PetServiceResources { /** * Add a new pet to the store. + * * Add a new pet to the store. */ public addPet( @@ -80,6 +348,7 @@ export class PetServiceResources { /** * Update an existing pet. + * * Update an existing pet by Id. */ public updatePet( @@ -93,6 +362,7 @@ export class PetServiceResources { /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ public findPetsByStatus( @@ -106,6 +376,7 @@ export class PetServiceResources { /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ public findPetsByTags( @@ -119,6 +390,7 @@ export class PetServiceResources { /** * Deletes a pet. + * * Delete a pet. */ public deletePet( @@ -132,6 +404,7 @@ export class PetServiceResources { /** * Find pet by ID. + * * Returns a single pet. */ public getPetById( @@ -145,6 +418,7 @@ export class PetServiceResources { /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ public updatePetWithForm( @@ -158,6 +432,7 @@ export class PetServiceResources { /** * Uploads an image. + * * Upload image of the pet. */ public uploadFile( @@ -176,6 +451,7 @@ export class PetServiceResources { export class StoreServiceResources { /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ public getInventory( @@ -189,6 +465,7 @@ export class StoreServiceResources { /** * Place an order for a pet. + * * Place a new order in the store. */ public placeOrder( @@ -202,6 +479,7 @@ export class StoreServiceResources { /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ public deleteOrder( @@ -215,6 +493,7 @@ export class StoreServiceResources { /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ public getOrderById( @@ -233,6 +512,7 @@ export class StoreServiceResources { export class UserServiceResources { /** * Create user. + * * This can only be done by the logged in user. */ public createUser( @@ -246,6 +526,7 @@ export class UserServiceResources { /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ public createUsersWithListInput( @@ -261,6 +542,7 @@ export class UserServiceResources { /** * Logs user into the system. + * * Log into the system. */ public loginUser( @@ -274,6 +556,7 @@ export class UserServiceResources { /** * Logs out current logged in user session. + * * Log user out of the system. */ public logoutUser( @@ -287,6 +570,7 @@ export class UserServiceResources { /** * Delete user resource. + * * This can only be done by the logged in user. */ public deleteUser( @@ -300,6 +584,7 @@ export class UserServiceResources { /** * Get user by user name. + * * Get user detail based on username. */ public getUserByName( @@ -313,6 +598,7 @@ export class UserServiceResources { /** * Update user resource. + * * This can only be done by the logged in user. */ public updateUser( diff --git a/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts b/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts deleted file mode 100644 index e66f2fbc8..000000000 --- a/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts +++ /dev/null @@ -1,295 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { HttpRequest } from '@angular/common/http'; - -import { client as _heyApiClient } from '../../../client.gen'; -import type { Options } from '../../../sdk.gen'; -import type { - AddPetData, - CreateUserData, - CreateUsersWithListInputData, - DeleteOrderData, - DeletePetData, - DeleteUserData, - FindPetsByStatusData, - FindPetsByTagsData, - GetInventoryData, - GetOrderByIdData, - GetPetByIdData, - GetUserByNameData, - LoginUserData, - LogoutUserData, - PlaceOrderData, - UpdatePetData, - UpdatePetWithFormData, - UpdateUserData, - UploadFileData, -} from '../../../types.gen'; - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet', - ...options, - }); - -/** - * Update an existing pet. - * Update an existing pet by Id. - */ -export const updatePetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'PUT', - responseStyle: 'data', - url: '/pet', - ...options, - }); - -/** - * Finds Pets by status. - * Multiple status values can be provided with comma separated strings. - */ -export const findPetsByStatusRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/findByStatus', - ...options, - }); - -/** - * Finds Pets by tags. - * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - */ -export const findPetsByTagsRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/findByTags', - ...options, - }); - -/** - * Deletes a pet. - * Delete a pet. - */ -export const deletePetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Find pet by ID. - * Returns a single pet. - */ -export const getPetByIdRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet/{petId}/uploadImage', - ...options, - }); - -/** - * Returns pet inventories by status. - * Returns a map of status codes to quantities. - */ -export const getInventoryRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/store/inventory', - ...options, - }); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/store/order', - ...options, - }); - -/** - * Delete purchase order by identifier. - * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. - */ -export const deleteOrderRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/store/order/{orderId}', - ...options, - }); - -/** - * Find purchase order by ID. - * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. - */ -export const getOrderByIdRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/store/order/{orderId}', - ...options, - }); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/user', - ...options, - }); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputRequest = < - ThrowOnError extends boolean = false, ->( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/user/createWithList', - ...options, - }); - -/** - * Logs user into the system. - * Log into the system. - */ -export const loginUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/login', - ...options, - }); - -/** - * Logs out current logged in user session. - * Log user out of the system. - */ -export const logoutUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/logout', - ...options, - }); - -/** - * Delete user resource. - * This can only be done by the logged in user. - */ -export const deleteUserRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); - -/** - * Get user by user name. - * Get user detail based on username. - */ -export const getUserByNameRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); - -/** - * Update user resource. - * This can only be done by the logged in user. - */ -export const updateUserRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'PUT', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); diff --git a/examples/openapi-ts-angular-common/src/client/client.gen.ts b/examples/openapi-ts-angular-common/src/client/client.gen.ts index 37fae577c..5bd9edd4b 100644 --- a/examples/openapi-ts-angular-common/src/client/client.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', throwOnError: true, }), diff --git a/examples/openapi-ts-angular-common/src/client/client/client.gen.ts b/examples/openapi-ts-angular-common/src/client/client/client.gen.ts index 93b77ace4..1555ca230 100644 --- a/examples/openapi-ts-angular-common/src/client/client/client.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client/client.gen.ts @@ -18,6 +18,7 @@ import { filter } from 'rxjs/operators'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; import type { Client, Config, @@ -69,7 +70,7 @@ export const createClient = (config: Config = {}): Client => { ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -83,12 +84,12 @@ export const createClient = (config: Config = {}): Client => { } } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } @@ -97,7 +98,7 @@ export const createClient = (config: Config = {}): Client => { const req = new HttpRequest( opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, @@ -130,7 +131,7 @@ export const createClient = (config: Config = {}): Client => { let req = initialReq; - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { req = await fn(req, opts as any); } @@ -151,7 +152,7 @@ export const createClient = (config: Config = {}): Client => { .pipe(filter((event) => event.type === HttpEventType.Response)), )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { result.response = await fn(result.response, req, opts as any); } @@ -177,7 +178,7 @@ export const createClient = (config: Config = {}): Client => { let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, diff --git a/examples/openapi-ts-angular-common/src/client/client/index.ts b/examples/openapi-ts-angular-common/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-angular-common/src/client/client/index.ts +++ b/examples/openapi-ts-angular-common/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts b/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts index b90ad7e5e..64a5d8b09 100644 --- a/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts @@ -345,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts b/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts index ac31396fe..0b5389d08 100644 --- a/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from './bodySerializer.gen'; +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; import { type ArraySeparatorStyle, serializeArrayParam, @@ -112,3 +112,32 @@ export const getUrl = ({ } return url; }; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-angular-common/src/client/index.ts b/examples/openapi-ts-angular-common/src/client/index.ts index 6921f209d..89fcd5868 100644 --- a/examples/openapi-ts-angular-common/src/client/index.ts +++ b/examples/openapi-ts-angular-common/src/client/index.ts @@ -1,5 +1,5 @@ // This file is auto-generated by @hey-api/openapi-ts -export * from './@angular/common/http/requests.gen'; -export * from './@angular/common/http/resources.gen'; + +export * from './@angular/common.gen'; export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-angular-common/src/client/sdk.gen.ts b/examples/openapi-ts-angular-common/src/client/sdk.gen.ts index 072abae41..d1c943a6c 100644 --- a/examples/openapi-ts-angular-common/src/client/sdk.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,12 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< AddPetResponses, AddPetErrors, ThrowOnError, @@ -109,12 +110,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError, @@ -137,12 +139,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError, @@ -161,12 +164,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError, @@ -185,12 +189,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError, @@ -209,12 +214,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError, @@ -237,12 +243,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError, @@ -261,12 +268,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError, @@ -290,12 +298,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError, @@ -314,12 +323,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError, @@ -336,12 +346,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError, @@ -354,12 +365,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError, @@ -372,12 +384,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError, @@ -394,12 +407,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError, @@ -416,12 +430,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError, @@ -434,12 +449,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError, @@ -452,12 +468,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError, @@ -470,12 +487,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError, @@ -488,12 +506,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError, diff --git a/examples/openapi-ts-angular-common/src/client/types.gen.ts b/examples/openapi-ts-angular-common/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-angular-common/src/client/types.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-angular/src/client/client.gen.ts b/examples/openapi-ts-angular/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-angular/src/client/client.gen.ts +++ b/examples/openapi-ts-angular/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-angular/src/client/client/client.gen.ts b/examples/openapi-ts-angular/src/client/client/client.gen.ts index 93b77ace4..1555ca230 100644 --- a/examples/openapi-ts-angular/src/client/client/client.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/client.gen.ts @@ -18,6 +18,7 @@ import { filter } from 'rxjs/operators'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; import type { Client, Config, @@ -69,7 +70,7 @@ export const createClient = (config: Config = {}): Client => { ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -83,12 +84,12 @@ export const createClient = (config: Config = {}): Client => { } } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } @@ -97,7 +98,7 @@ export const createClient = (config: Config = {}): Client => { const req = new HttpRequest( opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, @@ -130,7 +131,7 @@ export const createClient = (config: Config = {}): Client => { let req = initialReq; - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { req = await fn(req, opts as any); } @@ -151,7 +152,7 @@ export const createClient = (config: Config = {}): Client => { .pipe(filter((event) => event.type === HttpEventType.Response)), )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { result.response = await fn(result.response, req, opts as any); } @@ -177,7 +178,7 @@ export const createClient = (config: Config = {}): Client => { let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, diff --git a/examples/openapi-ts-angular/src/client/client/index.ts b/examples/openapi-ts-angular/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-angular/src/client/client/index.ts +++ b/examples/openapi-ts-angular/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-angular/src/client/client/utils.gen.ts b/examples/openapi-ts-angular/src/client/client/utils.gen.ts index b90ad7e5e..64a5d8b09 100644 --- a/examples/openapi-ts-angular/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/utils.gen.ts @@ -345,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-angular/src/client/core/utils.gen.ts b/examples/openapi-ts-angular/src/client/core/utils.gen.ts index ac31396fe..0b5389d08 100644 --- a/examples/openapi-ts-angular/src/client/core/utils.gen.ts +++ b/examples/openapi-ts-angular/src/client/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from './bodySerializer.gen'; +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; import { type ArraySeparatorStyle, serializeArrayParam, @@ -112,3 +112,32 @@ export const getUrl = ({ } return url; }; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-angular/src/client/index.ts b/examples/openapi-ts-angular/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-angular/src/client/index.ts +++ b/examples/openapi-ts-angular/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-angular/src/client/sdk.gen.ts b/examples/openapi-ts-angular/src/client/sdk.gen.ts index 2c40f5893..9d7d0b4e5 100644 --- a/examples/openapi-ts-angular/src/client/sdk.gen.ts +++ b/examples/openapi-ts-angular/src/client/sdk.gen.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -67,7 +67,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -87,12 +87,13 @@ export type Options< export class PetService { /** * Add a new pet to the store. + * * Add a new pet to the store. */ public addPet( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< AddPetResponses, AddPetErrors, ThrowOnError @@ -114,12 +115,13 @@ export class PetService { /** * Update an existing pet. + * * Update an existing pet by Id. */ public updatePet( options: Options, ) { - return (options.client ?? _heyApiClient).put< + return (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -141,12 +143,13 @@ export class PetService { /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ public findPetsByStatus( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -164,12 +167,13 @@ export class PetService { /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ public findPetsByTags( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -187,12 +191,13 @@ export class PetService { /** * Deletes a pet. + * * Delete a pet. */ public deletePet( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -210,12 +215,13 @@ export class PetService { /** * Find pet by ID. + * * Returns a single pet. */ public getPetById( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -237,12 +243,13 @@ export class PetService { /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ public updatePetWithForm( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -260,12 +267,13 @@ export class PetService { /** * Uploads an image. + * * Upload image of the pet. */ public uploadFile( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -293,12 +301,13 @@ export class PetService { export class StoreService { /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ public getInventory( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -316,12 +325,13 @@ export class StoreService { /** * Place an order for a pet. + * * Place a new order in the store. */ public placeOrder( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -337,12 +347,13 @@ export class StoreService { /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ public deleteOrder( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -354,12 +365,13 @@ export class StoreService { /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ public getOrderById( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -376,12 +388,13 @@ export class StoreService { export class UserService { /** * Create user. + * * This can only be done by the logged in user. */ public createUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -397,12 +410,13 @@ export class UserService { /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ public createUsersWithListInput( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -418,12 +432,13 @@ export class UserService { /** * Logs user into the system. + * * Log into the system. */ public loginUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -435,12 +450,13 @@ export class UserService { /** * Logs out current logged in user session. + * * Log user out of the system. */ public logoutUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -452,12 +468,13 @@ export class UserService { /** * Delete user resource. + * * This can only be done by the logged in user. */ public deleteUser( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -469,12 +486,13 @@ export class UserService { /** * Get user by user name. + * * Get user detail based on username. */ public getUserByName( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -486,12 +504,13 @@ export class UserService { /** * Update user resource. + * * This can only be done by the logged in user. */ public updateUser( options: Options, ) { - return (options.client ?? _heyApiClient).put< + return (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-angular/src/client/types.gen.ts b/examples/openapi-ts-angular/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-angular/src/client/types.gen.ts +++ b/examples/openapi-ts-angular/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-axios/src/client/client.gen.ts b/examples/openapi-ts-axios/src/client/client.gen.ts index 102ab4bfb..c8bfd460b 100644 --- a/examples/openapi-ts-axios/src/client/client.gen.ts +++ b/examples/openapi-ts-axios/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseURL: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-axios/src/client/client/client.gen.ts b/examples/openapi-ts-axios/src/client/client/client.gen.ts index b2f7b118f..aea2484c7 100644 --- a/examples/openapi-ts-axios/src/client/client/client.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/client.gen.ts @@ -1,9 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios from 'axios'; +import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios, { AxiosError } from 'axios'; -import type { Client, Config } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions } from './types.gen'; import { buildUrl, createConfig, @@ -38,8 +41,7 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - // @ts-expect-error - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, @@ -58,12 +60,18 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.body = opts.bodySerializer(opts.body); } const url = buildUrl(opts); + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); try { // assign Axios here for consistency with fetch const _axios = opts.axios!; @@ -71,13 +79,14 @@ export const createClient = (config: Config = {}): Client => { const { auth, ...optsWithoutAuth } = opts; const response = await _axios({ ...optsWithoutAuth, - baseURL: opts.baseURL as string, - data: opts.body, + baseURL: '', // the baseURL is already included in `url` + data: getValidRequestBody(opts), headers: opts.headers as RawAxiosRequestHeaders, // let `paramsSerializer()` handle query params if it exists params: opts.paramsSerializer ? opts.query : undefined, url, }); + if (response instanceof Error) throw response; let { data } = response; @@ -96,28 +105,68 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { - const e = error as AxiosError; if (opts.throwOnError) { - throw e; + throw error; } - // @ts-expect-error - e.error = e.response?.data ?? {}; - return e; + + if (error instanceof AxiosError) { + // @ts-expect-error + error.error = error.response?.data ?? {}; + return error; + } + + if (typeof error === 'object' && error !== null) { + error.error = {}; + return error; + } + + return { error: {} }; } }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as Record, + method, + // @ts-expect-error + signal: opts.signal, + url, + }); + }; + return { buildUrl, - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), instance, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), } as Client; }; diff --git a/examples/openapi-ts-axios/src/client/client/index.ts b/examples/openapi-ts-axios/src/client/client/index.ts index 8ddc04f42..cff1d39c9 100644 --- a/examples/openapi-ts-axios/src/client/client/index.ts +++ b/examples/openapi-ts-axios/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-axios/src/client/client/types.gen.ts b/examples/openapi-ts-axios/src/client/client/types.gen.ts index b28841acc..d59239b9a 100644 --- a/examples/openapi-ts-axios/src/client/client/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/types.gen.ts @@ -10,6 +10,10 @@ import type { } from 'axios'; import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -56,11 +60,20 @@ export interface Config } export interface RequestOptions< + TData = unknown, ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - throwOnError: ThrowOnError; - }> { + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -76,6 +89,11 @@ export interface RequestOptions< url: Url; } +export interface ClientOptions { + baseURL?: string; + throwOnError?: boolean; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -100,26 +118,29 @@ export type RequestResult< }) >; -export interface ClientOptions { - baseURL?: string; - throwOnError?: boolean; -} - type MethodFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < @@ -130,10 +151,16 @@ type BuildUrlFn = < url: string; }, >( - options: Pick & Omit, 'axios'>, + options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { instance: AxiosInstance; }; @@ -162,7 +189,11 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + TResponse = unknown, +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit; export type OptionsLegacyParser< @@ -170,12 +201,16 @@ export type OptionsLegacyParser< ThrowOnError extends boolean = boolean, > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-axios/src/client/client/utils.gen.ts b/examples/openapi-ts-axios/src/client/client/utils.gen.ts index 8f20fa853..c87309246 100644 --- a/examples/openapi-ts-axios/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/utils.gen.ts @@ -1,16 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from '../core/auth.gen'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer.gen'; -import type { ArraySeparatorStyle } from '../core/pathSerializer.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; import type { Client, ClientOptions, @@ -18,83 +15,6 @@ import type { RequestOptions, } from './types.gen'; -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - export const createQuerySerializer = ({ allowReserved, array, @@ -211,7 +131,15 @@ export const setAuthParams = async ({ }; export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ + const instanceBaseUrl = options.axios?.defaults?.baseURL; + + const baseUrl = + !!options.baseURL && typeof options.baseURL === 'string' + ? options.baseURL + : instanceBaseUrl; + + return getUrl({ + baseUrl: baseUrl as string, path: options.path, // let `paramsSerializer()` handle query params if it exists query: !options.paramsSerializer ? options.query : undefined, @@ -221,33 +149,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - path, - query, - querySerializer, - url: _url, -}: { - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; }; export const mergeConfigs = (a: Config, b: Config): Config => { diff --git a/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-axios/src/client/core/types.gen.ts b/examples/openapi-ts-axios/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-axios/src/client/core/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-axios/src/client/core/utils.gen.ts b/examples/openapi-ts-axios/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-axios/src/client/index.ts b/examples/openapi-ts-axios/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-axios/src/client/index.ts +++ b/examples/openapi-ts-axios/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-axios/src/client/sdk.gen.ts b/examples/openapi-ts-axios/src/client/sdk.gen.ts index 59074a4f4..bb7cee4ba 100644 --- a/examples/openapi-ts-axios/src/client/sdk.gen.ts +++ b/examples/openapi-ts-axios/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ responseType: 'json', security: [ { @@ -108,12 +105,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -135,12 +133,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -158,12 +157,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -181,12 +181,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -203,12 +204,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -230,12 +232,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -253,12 +256,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -281,12 +285,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -304,12 +309,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -325,12 +331,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -341,12 +348,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -358,12 +366,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -379,12 +388,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -400,12 +410,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -417,12 +428,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -433,12 +445,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -449,12 +462,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -466,12 +480,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-axios/src/client/types.gen.ts b/examples/openapi-ts-axios/src/client/types.gen.ts index 257d5446a..48da19d87 100644 --- a/examples/openapi-ts-axios/src/client/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseURL: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseURL: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-fastify/src/client/client.gen.ts b/examples/openapi-ts-fastify/src/client/client.gen.ts index 6061f9286..db8c98964 100644 --- a/examples/openapi-ts-fastify/src/client/client.gen.ts +++ b/examples/openapi-ts-fastify/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'http://petstore.swagger.io/v1', }), ); diff --git a/examples/openapi-ts-sample/src/client/client/client.ts b/examples/openapi-ts-fastify/src/client/client/client.gen.ts similarity index 56% rename from examples/openapi-ts-sample/src/client/client/client.ts rename to examples/openapi-ts-fastify/src/client/client/client.gen.ts index 89d1e3158..a439d2748 100644 --- a/examples/openapi-ts-sample/src/client/client/client.ts +++ b/examples/openapi-ts-fastify/src/client/client/client.gen.ts @@ -1,4 +1,14 @@ -import type { Client, Config, RequestOptions } from './types'; +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -7,7 +17,7 @@ import { mergeConfigs, mergeHeaders, setAuthParams, -} from './utils'; +} from './utils.gen'; type ReqInit = Omit & { body?: any; @@ -28,15 +38,16 @@ export const createClient = (config: Config = {}): Client => { Request, Response, unknown, - RequestOptions + ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, }; if (opts.security) { @@ -50,24 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -78,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -90,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -155,7 +192,7 @@ export const createClient = (config: Config = {}): Client => { const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -176,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-fastify/src/client/client/client.ts b/examples/openapi-ts-fastify/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-fastify/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-fastify/src/client/client/index.ts b/examples/openapi-ts-fastify/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-fastify/src/client/client/index.ts +++ b/examples/openapi-ts-fastify/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-fetch/src/client/client/types.ts b/examples/openapi-ts-fastify/src/client/client/types.gen.ts similarity index 70% rename from examples/openapi-ts-fetch/src/client/client/types.ts rename to examples/openapi-ts-fastify/src/client/client/types.gen.ts index c39a93132..1a005b51e 100644 --- a/examples/openapi-ts-fetch/src/client/client/types.ts +++ b/examples/openapi-ts-fastify/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -56,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -78,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -135,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -159,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -188,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -202,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-sample/src/client/client/utils.ts b/examples/openapi-ts-fastify/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-sample/src/client/client/utils.ts rename to examples/openapi-ts-fastify/src/client/client/utils.gen.ts index a52e67292..96de282a8 100644 --- a/examples/openapi-ts-sample/src/client/client/utils.ts +++ b/examples/openapi-ts-fastify/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -186,6 +110,25 @@ export const getParseAs = ( return; }; +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + export const setAuthParams = async ({ security, ...options @@ -194,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -217,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -233,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -273,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -324,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-fastify/src/client/core/auth.ts b/examples/openapi-ts-fastify/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-fastify/src/client/core/auth.ts rename to examples/openapi-ts-fastify/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-fastify/src/client/core/auth.ts +++ b/examples/openapi-ts-fastify/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-sample/src/client/core/bodySerializer.ts b/examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts similarity index 92% rename from examples/openapi-ts-sample/src/client/core/bodySerializer.ts rename to examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts index 98ce7791f..49cd8925e 100644 --- a/examples/openapi-ts-sample/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -21,6 +23,8 @@ const serializeFormDataPair = ( ): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } diff --git a/examples/openapi-ts-sample/src/client/core/params.ts b/examples/openapi-ts-fastify/src/client/core/params.gen.ts similarity index 98% rename from examples/openapi-ts-sample/src/client/core/params.ts rename to examples/openapi-ts-fastify/src/client/core/params.gen.ts index ba35263d8..71c88e852 100644 --- a/examples/openapi-ts-sample/src/client/core/params.ts +++ b/examples/openapi-ts-fastify/src/client/core/params.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = diff --git a/examples/openapi-ts-next/src/client/core/pathSerializer.ts b/examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-next/src/client/core/pathSerializer.ts rename to examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-next/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-sample/src/client/core/types.ts b/examples/openapi-ts-fastify/src/client/core/types.gen.ts similarity index 85% rename from examples/openapi-ts-sample/src/client/core/types.ts rename to examples/openapi-ts-fastify/src/client/core/types.gen.ts index 2dd4106fb..643c070c9 100644 --- a/examples/openapi-ts-sample/src/client/core/types.ts +++ b/examples/openapi-ts-fastify/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-fastify/src/client/core/utils.gen.ts b/examples/openapi-ts-fastify/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-fastify/src/client/index.ts b/examples/openapi-ts-fastify/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-fastify/src/client/index.ts +++ b/examples/openapi-ts-fastify/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-fastify/src/client/sdk.gen.ts b/examples/openapi-ts-fastify/src/client/sdk.gen.ts index 05760e098..c60e38505 100644 --- a/examples/openapi-ts-fastify/src/client/sdk.gen.ts +++ b/examples/openapi-ts-fastify/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { CreatePetsData, CreatePetsErrors, @@ -17,7 +17,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -37,7 +37,7 @@ export type Options< export const listPets = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< ListPetsResponses, ListPetsErrors, ThrowOnError @@ -52,7 +52,7 @@ export const listPets = ( export const createPets = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreatePetsResponses, CreatePetsErrors, ThrowOnError @@ -67,7 +67,7 @@ export const createPets = ( export const showPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< ShowPetByIdResponses, ShowPetByIdErrors, ThrowOnError diff --git a/examples/openapi-ts-fastify/src/client/types.gen.ts b/examples/openapi-ts-fastify/src/client/types.gen.ts index a05af4065..054b6615e 100644 --- a/examples/openapi-ts-fastify/src/client/types.gen.ts +++ b/examples/openapi-ts-fastify/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'http://petstore.swagger.io/v1' | (string & {}); +}; + export type Pet = { id: number; name: string; @@ -96,7 +100,3 @@ export type ShowPetByIdResponses = { export type ShowPetByIdResponse = ShowPetByIdResponses[keyof ShowPetByIdResponses]; - -export type ClientOptions = { - baseUrl: 'http://petstore.swagger.io/v1' | (string & {}); -}; diff --git a/examples/openapi-ts-fetch/src/client/client.gen.ts b/examples/openapi-ts-fetch/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-fetch/src/client/client.gen.ts +++ b/examples/openapi-ts-fetch/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-fetch/src/client/client/client.ts b/examples/openapi-ts-fetch/src/client/client/client.gen.ts similarity index 54% rename from examples/openapi-ts-fetch/src/client/client/client.ts rename to examples/openapi-ts-fetch/src/client/client/client.gen.ts index dddfe80c0..a439d2748 100644 --- a/examples/openapi-ts-fetch/src/client/client/client.ts +++ b/examples/openapi-ts-fetch/src/client/client/client.gen.ts @@ -1,4 +1,14 @@ -import type { Client, Config, RequestOptions } from './types'; +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -7,7 +17,7 @@ import { mergeConfigs, mergeHeaders, setAuthParams, -} from './utils'; +} from './utils.gen'; type ReqInit = Omit & { body?: any; @@ -28,15 +38,16 @@ export const createClient = (config: Config = {}): Client => { Request, Response, unknown, - RequestOptions + ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, }; if (opts.security) { @@ -50,24 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -78,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -90,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -143,17 +180,19 @@ export const createClient = (config: Config = {}): Client => { }; } - let error = await response.text(); + const textError = await response.text(); + let jsonError: unknown; try { - error = JSON.parse(error); + jsonError = JSON.parse(textError); } catch { // noop } + const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -174,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-fetch/src/client/client/index.ts b/examples/openapi-ts-fetch/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-fetch/src/client/client/index.ts +++ b/examples/openapi-ts-fetch/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-sample/src/client/client/types.ts b/examples/openapi-ts-fetch/src/client/client/types.gen.ts similarity index 70% rename from examples/openapi-ts-sample/src/client/client/types.ts rename to examples/openapi-ts-fetch/src/client/client/types.gen.ts index c39a93132..1a005b51e 100644 --- a/examples/openapi-ts-sample/src/client/client/types.ts +++ b/examples/openapi-ts-fetch/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -56,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -78,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -135,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -159,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -188,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -202,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-fetch/src/client/client/utils.ts b/examples/openapi-ts-fetch/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-fetch/src/client/client/utils.ts rename to examples/openapi-ts-fetch/src/client/client/utils.gen.ts index a52e67292..96de282a8 100644 --- a/examples/openapi-ts-fetch/src/client/client/utils.ts +++ b/examples/openapi-ts-fetch/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -186,6 +110,25 @@ export const getParseAs = ( return; }; +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + export const setAuthParams = async ({ security, ...options @@ -194,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -217,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -233,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -273,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -324,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-fetch/src/client/core/auth.ts b/examples/openapi-ts-fetch/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-fetch/src/client/core/auth.ts rename to examples/openapi-ts-fetch/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-fetch/src/client/core/auth.ts +++ b/examples/openapi-ts-fetch/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-fetch/src/client/core/bodySerializer.ts b/examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts similarity index 84% rename from examples/openapi-ts-fetch/src/client/core/bodySerializer.ts rename to examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts index 21c235741..49cd8925e 100644 --- a/examples/openapi-ts-fetch/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,7 +64,7 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => + bodySerializer: (body: T): string => JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-fetch/src/client/core/params.ts b/examples/openapi-ts-fetch/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-fetch/src/client/core/params.ts rename to examples/openapi-ts-fetch/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-fetch/src/client/core/params.ts +++ b/examples/openapi-ts-fetch/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-sample/src/client/core/pathSerializer.ts b/examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-sample/src/client/core/pathSerializer.ts rename to examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-sample/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/types.ts b/examples/openapi-ts-fetch/src/client/core/types.gen.ts similarity index 75% rename from examples/openapi-ts-fetch/src/client/core/types.ts rename to examples/openapi-ts-fetch/src/client/core/types.gen.ts index 77d879253..643c070c9 100644 --- a/examples/openapi-ts-fetch/src/client/core/types.ts +++ b/examples/openapi-ts-fetch/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -102,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/utils.gen.ts b/examples/openapi-ts-fetch/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-fetch/src/client/index.ts b/examples/openapi-ts-fetch/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-fetch/src/client/index.ts +++ b/examples/openapi-ts-fetch/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-fetch/src/client/sdk.gen.ts b/examples/openapi-ts-fetch/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-fetch/src/client/sdk.gen.ts +++ b/examples/openapi-ts-fetch/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-fetch/src/client/types.gen.ts b/examples/openapi-ts-fetch/src/client/types.gen.ts index 6d8a6e7b9..a2e6be0fa 100644 --- a/examples/openapi-ts-fetch/src/client/types.gen.ts +++ b/examples/openapi-ts-fetch/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-next/src/client/client.gen.ts b/examples/openapi-ts-next/src/client/client.gen.ts index 25aa4ccc9..4e785697c 100644 --- a/examples/openapi-ts-next/src/client/client.gen.ts +++ b/examples/openapi-ts-next/src/client/client.gen.ts @@ -1,13 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts -import { createClientConfig } from '../hey-api'; import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import { createClientConfig } from './src/hey-api.ts'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -17,14 +17,13 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( createClientConfig( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ), diff --git a/examples/openapi-ts-next/src/client/client/client.gen.ts b/examples/openapi-ts-next/src/client/client/client.gen.ts new file mode 100644 index 000000000..53d0433b9 --- /dev/null +++ b/examples/openapi-ts-next/src/client/client/client.gen.ts @@ -0,0 +1,262 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + + for (const fn of interceptors.request.fns) { + if (fn) { + await fn(opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + const requestInit: ReqInit = { + ...opts, + body: getValidRequestBody(opts), + }; + + let response = await _fetch(url, requestInit); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, opts); + } + } + + const result = { + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + return { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + const requestInit = { + ...init, + method: init.method as Config['method'], + url, + }; + for (const fn of interceptors.request.fns) { + if (fn) { + await fn(requestInit); + request = new Request(requestInit.url, requestInit); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-next/src/client/client/client.ts b/examples/openapi-ts-next/src/client/client/client.ts deleted file mode 100644 index acd447624..000000000 --- a/examples/openapi-ts-next/src/client/client/client.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - // @ts-expect-error - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - for (const fn of interceptors.request._fns) { - if (fn) { - await fn(opts); - } - } - - const url = buildUrl(opts); - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(url, { - ...opts, - body: opts.body as ReqInit['body'], - }); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, opts); - } - } - - const result = { - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - return { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-next/src/client/client/index.ts b/examples/openapi-ts-next/src/client/client/index.ts index 15d37422a..cff1d39c9 100644 --- a/examples/openapi-ts-next/src/client/client/index.ts +++ b/examples/openapi-ts-next/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -17,5 +20,5 @@ export type { RequestOptions, RequestResult, TDataShape, -} from './types'; -export { createConfig } from './utils'; +} from './types.gen'; +export { createConfig } from './utils.gen'; diff --git a/examples/openapi-ts-next/src/client/client/types.ts b/examples/openapi-ts-next/src/client/client/types.gen.ts similarity index 64% rename from examples/openapi-ts-next/src/client/client/types.ts rename to examples/openapi-ts-next/src/client/client/types.gen.ts index aa7539213..76f0bf322 100644 --- a/examples/openapi-ts-next/src/client/client/types.ts +++ b/examples/openapi-ts-next/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export interface Config extends Omit, @@ -24,7 +33,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Throw an error instead of returning it in the response? * @@ -34,11 +50,20 @@ export interface Config } export interface RequestOptions< + TData = unknown, ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - throwOnError: ThrowOnError; - }> { + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -54,6 +79,13 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -92,16 +124,24 @@ type MethodFn = < TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < @@ -115,8 +155,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -144,7 +190,11 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + TResponse = unknown, +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit; export type OptionsLegacyParser< @@ -152,12 +202,16 @@ export type OptionsLegacyParser< ThrowOnError extends boolean = boolean, > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-next/src/client/client/utils.ts b/examples/openapi-ts-next/src/client/client/utils.gen.ts similarity index 81% rename from examples/openapi-ts-next/src/client/client/utils.ts rename to examples/openapi-ts-next/src/client/client/utils.gen.ts index 004023033..914ee288d 100644 --- a/examples/openapi-ts-next/src/client/client/utils.ts +++ b/examples/openapi-ts-next/src/client/client/utils.gen.ts @@ -1,15 +1,22 @@ -import { getAuthToken } from '../core/auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; import type { QuerySerializer, QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +} from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; +} from '../core/pathSerializer.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; interface PathSerializer { path: Record; @@ -182,6 +189,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +220,9 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,8 +246,6 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; @@ -271,6 +300,14 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { @@ -281,7 +318,9 @@ export const mergeHeaders = ( } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -317,61 +356,60 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick>, 'eject' | 'use'>; - request: Pick>, 'eject' | 'use'>; - response: Pick>, 'eject' | 'use'>; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-next/src/client/core/auth.ts b/examples/openapi-ts-next/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-next/src/client/core/auth.ts rename to examples/openapi-ts-next/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-next/src/client/core/auth.ts +++ b/examples/openapi-ts-next/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-fastify/src/client/core/bodySerializer.ts b/examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts similarity index 82% rename from examples/openapi-ts-fastify/src/client/core/bodySerializer.ts rename to examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts index fab971b66..49cd8925e 100644 --- a/examples/openapi-ts-fastify/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,8 +64,8 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), }; @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-fastify/src/client/core/params.ts b/examples/openapi-ts-next/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-fastify/src/client/core/params.ts rename to examples/openapi-ts-next/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-fastify/src/client/core/params.ts +++ b/examples/openapi-ts-next/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-fetch/src/client/core/pathSerializer.ts b/examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-fetch/src/client/core/pathSerializer.ts rename to examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-fetch/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-fastify/src/client/core/types.ts b/examples/openapi-ts-next/src/client/core/types.gen.ts similarity index 68% rename from examples/openapi-ts-fastify/src/client/core/types.ts rename to examples/openapi-ts-next/src/client/core/types.gen.ts index 1f8688099..643c070c9 100644 --- a/examples/openapi-ts-fastify/src/client/core/types.ts +++ b/examples/openapi-ts-next/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -84,6 +84,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -96,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-next/src/client/core/utils.gen.ts b/examples/openapi-ts-next/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-next/src/client/index.ts b/examples/openapi-ts-next/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-next/src/client/index.ts +++ b/examples/openapi-ts-next/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-next/src/client/sdk.gen.ts b/examples/openapi-ts-next/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-next/src/client/sdk.gen.ts +++ b/examples/openapi-ts-next/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-next/src/client/types.gen.ts b/examples/openapi-ts-next/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-next/src/client/types.gen.ts +++ b/examples/openapi-ts-next/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-ofetch/src/client/client/index.ts b/examples/openapi-ts-ofetch/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-ofetch/src/client/client/index.ts +++ b/examples/openapi-ts-ofetch/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-ofetch/src/client/sdk.gen.ts b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts index 4bb50da39..f424fe675 100644 --- a/examples/openapi-ts-ofetch/src/client/sdk.gen.ts +++ b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts @@ -81,6 +81,7 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( @@ -103,6 +104,7 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( @@ -129,6 +131,7 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( @@ -151,6 +154,7 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( @@ -173,6 +177,7 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( @@ -195,6 +200,7 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( @@ -221,6 +227,7 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( @@ -243,6 +250,7 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( @@ -270,6 +278,7 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( @@ -292,6 +301,7 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( @@ -312,6 +322,7 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( @@ -328,6 +339,7 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( @@ -344,6 +356,7 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( @@ -364,6 +377,7 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( @@ -384,6 +398,7 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( @@ -400,6 +415,7 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( @@ -416,6 +432,7 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( @@ -432,6 +449,7 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( @@ -448,6 +466,7 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( diff --git a/examples/openapi-ts-openai/src/client/client.gen.ts b/examples/openapi-ts-openai/src/client/client.gen.ts index ed96ef557..60a14f713 100644 --- a/examples/openapi-ts-openai/src/client/client.gen.ts +++ b/examples/openapi-ts-openai/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://api.openai.com/v1', }), ); diff --git a/examples/openapi-ts-openai/src/client/client/client.gen.ts b/examples/openapi-ts-openai/src/client/client/client.gen.ts index 0c606b81c..a439d2748 100644 --- a/examples/openapi-ts-openai/src/client/client/client.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/client.gen.ts @@ -1,6 +1,14 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -33,7 +41,7 @@ export const createClient = (config: Config = {}): Client => { ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, @@ -53,25 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, - body: opts.serializedBody, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -82,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -94,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -159,7 +192,7 @@ export const createClient = (config: Config = {}): Client => { const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -180,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-openai/src/client/client/index.ts b/examples/openapi-ts-openai/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-openai/src/client/client/index.ts +++ b/examples/openapi-ts-openai/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-openai/src/client/client/types.gen.ts b/examples/openapi-ts-openai/src/client/client/types.gen.ts index 2a123be9a..1a005b51e 100644 --- a/examples/openapi-ts-openai/src/client/client/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/types.gen.ts @@ -1,6 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -22,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -61,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -87,7 +100,7 @@ export interface ResolvedRequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> extends RequestOptions { +> extends RequestOptions { serializedBody?: string; } @@ -148,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -172,7 +197,13 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { interceptors: Middleware; }; @@ -201,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -215,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-openai/src/client/client/utils.gen.ts b/examples/openapi-ts-openai/src/client/client/utils.gen.ts index 6f955d080..96de282a8 100644 --- a/examples/openapi-ts-openai/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/utils.gen.ts @@ -1,16 +1,14 @@ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from '../core/auth.gen'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; import type { Client, ClientOptions, @@ -18,87 +16,6 @@ import type { RequestOptions, } from './types.gen'; -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - export const createQuerySerializer = ({ allowReserved, array, @@ -250,8 +167,8 @@ export const setAuthParams = async ({ } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -261,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -301,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -352,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-openai/src/client/core/types.gen.ts b/examples/openapi-ts-openai/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-openai/src/client/core/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-openai/src/client/core/utils.gen.ts b/examples/openapi-ts-openai/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-openai/src/client/index.ts b/examples/openapi-ts-openai/src/client/index.ts index 688e3c912..f796d2cc8 100644 --- a/examples/openapi-ts-openai/src/client/index.ts +++ b/examples/openapi-ts-openai/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; export * from './types.gen'; diff --git a/examples/openapi-ts-openai/src/client/sdk.gen.ts b/examples/openapi-ts-openai/src/client/sdk.gen.ts index 6bcc5384d..97180dae3 100644 --- a/examples/openapi-ts-openai/src/client/sdk.gen.ts +++ b/examples/openapi-ts-openai/src/client/sdk.gen.ts @@ -3,10 +3,10 @@ import { type Client, formDataBodySerializer, - type Options as ClientOptions, + type Options as Options2, type TDataShape, } from './client'; -import { client as _heyApiClient } from './client.gen'; +import { client } from './client.gen'; import type { ActivateOrganizationCertificatesData, ActivateOrganizationCertificatesResponses, @@ -351,7 +351,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -366,7 +366,7 @@ export type Options< }; class _HeyApiClient { - protected _client: Client = _heyApiClient; + protected _client: Client = client; constructor(args?: { client?: Client }) { if (args?.client) { @@ -378,6 +378,7 @@ class _HeyApiClient { export class OpenAi extends _HeyApiClient { /** * List assistants + * * Returns a list of assistants. */ public listAssistants( @@ -401,6 +402,7 @@ export class OpenAi extends _HeyApiClient { /** * Create assistant + * * Create an assistant with a model and instructions. */ public createAssistant( @@ -428,6 +430,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete assistant + * * Delete an assistant. */ public deleteAssistant( @@ -451,6 +454,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve assistant + * * Retrieves an assistant. */ public getAssistant( @@ -474,6 +478,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify assistant + * * Modifies an assistant. */ public modifyAssistant( @@ -501,6 +506,7 @@ export class OpenAi extends _HeyApiClient { /** * Create speech + * * Generates audio from the input text. */ public createSpeech( @@ -528,6 +534,7 @@ export class OpenAi extends _HeyApiClient { /** * Create transcription + * * Transcribes audio into the input language. */ public createTranscription( @@ -556,6 +563,7 @@ export class OpenAi extends _HeyApiClient { /** * Create translation + * * Translates audio into English. */ public createTranslation( @@ -584,6 +592,7 @@ export class OpenAi extends _HeyApiClient { /** * List batch + * * List your organization's batches. */ public listBatches( @@ -607,6 +616,7 @@ export class OpenAi extends _HeyApiClient { /** * Create batch + * * Creates and executes a batch from an uploaded file of requests */ public createBatch( @@ -634,6 +644,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve batch + * * Retrieves a batch. */ public retrieveBatch( @@ -657,6 +668,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel batch + * * Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file. */ public cancelBatch( @@ -680,6 +692,7 @@ export class OpenAi extends _HeyApiClient { /** * List Chat Completions + * * List stored Chat Completions. Only Chat Completions that have been stored * with the `store` parameter set to `true` will be returned. * @@ -705,6 +718,7 @@ export class OpenAi extends _HeyApiClient { /** * Create chat completion + * * **Starting a new project?** We recommend trying [Responses](https://platform.openai.com/docs/api-reference/responses) * to take advantage of the latest OpenAI platform features. Compare * [Chat Completions with Responses](https://platform.openai.com/docs/guides/responses-vs-chat-completions?api-mode=responses). @@ -747,6 +761,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete chat completion + * * Delete a stored chat completion. Only Chat Completions that have been * created with the `store` parameter set to `true` can be deleted. * @@ -772,6 +787,7 @@ export class OpenAi extends _HeyApiClient { /** * Get chat completion + * * Get a stored chat completion. Only Chat Completions that have been created * with the `store` parameter set to `true` will be returned. * @@ -797,6 +813,7 @@ export class OpenAi extends _HeyApiClient { /** * Update chat completion + * * Modify a stored chat completion. Only Chat Completions that have been * created with the `store` parameter set to `true` can be modified. Currently, * the only supported modification is to update the `metadata` field. @@ -827,6 +844,7 @@ export class OpenAi extends _HeyApiClient { /** * Get chat messages + * * Get the messages in a stored chat completion. Only Chat Completions that * have been created with the `store` parameter set to `true` will be * returned. @@ -853,6 +871,7 @@ export class OpenAi extends _HeyApiClient { /** * Create completion + * * Creates a completion for the provided prompt and parameters. */ public createCompletion( @@ -880,6 +899,7 @@ export class OpenAi extends _HeyApiClient { /** * List containers + * * List Containers */ public listContainers( @@ -903,6 +923,7 @@ export class OpenAi extends _HeyApiClient { /** * Create container + * * Create Container */ public createContainer( @@ -930,6 +951,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a container + * * Delete Container */ public deleteContainer( @@ -953,6 +975,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container + * * Retrieve Container */ public retrieveContainer( @@ -976,6 +999,7 @@ export class OpenAi extends _HeyApiClient { /** * List container files + * * List Container files */ public listContainerFiles( @@ -999,6 +1023,7 @@ export class OpenAi extends _HeyApiClient { /** * Create container file + * * Create a Container File * * You can send either a multipart/form-data request with the raw file content, or a JSON request with a file ID. @@ -1030,6 +1055,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a container file + * * Delete Container File */ public deleteContainerFile( @@ -1053,6 +1079,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container file + * * Retrieve Container File */ public retrieveContainerFile( @@ -1076,6 +1103,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container file content + * * Retrieve Container File Content */ public retrieveContainerFileContent( @@ -1099,6 +1127,7 @@ export class OpenAi extends _HeyApiClient { /** * Create embeddings + * * Creates an embedding vector representing the input text. */ public createEmbedding( @@ -1126,6 +1155,7 @@ export class OpenAi extends _HeyApiClient { /** * List evals + * * List evaluations for a project. * */ @@ -1150,6 +1180,7 @@ export class OpenAi extends _HeyApiClient { /** * Create eval + * * Create the structure of an evaluation that can be used to test a model's performance. * An evaluation is a set of testing criteria and the config for a data source, which dictates the schema of the data used in the evaluation. After creating an evaluation, you can run it on different models and model parameters. We support several types of graders and datasources. * For more information, see the [Evals guide](https://platform.openai.com/docs/guides/evals). @@ -1180,6 +1211,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete an eval + * * Delete an evaluation. * */ @@ -1204,6 +1236,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an eval + * * Get an evaluation by ID. * */ @@ -1228,6 +1261,7 @@ export class OpenAi extends _HeyApiClient { /** * Update an eval + * * Update certain properties of an evaluation. * */ @@ -1256,6 +1290,7 @@ export class OpenAi extends _HeyApiClient { /** * Get eval runs + * * Get a list of runs for an evaluation. * */ @@ -1280,6 +1315,7 @@ export class OpenAi extends _HeyApiClient { /** * Create eval run + * * Kicks off a new run for a given evaluation, specifying the data source, and what model configuration to use to test. The datasource will be validated against the schema specified in the config of the evaluation. * */ @@ -1308,6 +1344,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete eval run + * * Delete an eval run. * */ @@ -1332,6 +1369,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an eval run + * * Get an evaluation run by ID. * */ @@ -1356,6 +1394,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel eval run + * * Cancel an ongoing evaluation run. * */ @@ -1380,6 +1419,7 @@ export class OpenAi extends _HeyApiClient { /** * Get eval run output items + * * Get a list of output items for an evaluation run. * */ @@ -1404,6 +1444,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an output item of an eval run + * * Get an evaluation run output item by ID. * */ @@ -1428,6 +1469,7 @@ export class OpenAi extends _HeyApiClient { /** * List files + * * Returns a list of files. */ public listFiles( @@ -1451,6 +1493,7 @@ export class OpenAi extends _HeyApiClient { /** * Upload file + * * Upload a file that can be used across various endpoints. Individual files can be up to 512 MB, and the size of all files uploaded by one organization can be up to 1 TB. * * The Assistants API supports files up to 2 million tokens and of specific file types. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) for details. @@ -1488,6 +1531,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete file + * * Delete a file. */ public deleteFile( @@ -1511,6 +1555,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve file + * * Returns information about a specific file. */ public retrieveFile( @@ -1534,6 +1579,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve file content + * * Returns the contents of the specified file. */ public downloadFile( @@ -1557,6 +1603,7 @@ export class OpenAi extends _HeyApiClient { /** * Run grader + * * Run a grader. * */ @@ -1585,6 +1632,7 @@ export class OpenAi extends _HeyApiClient { /** * Validate grader + * * Validate a grader. * */ @@ -1613,6 +1661,7 @@ export class OpenAi extends _HeyApiClient { /** * List checkpoint permissions + * * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). * * Organization owners can use this endpoint to view all permissions for a fine-tuned model checkpoint. @@ -1639,6 +1688,7 @@ export class OpenAi extends _HeyApiClient { /** * Create checkpoint permissions + * * **NOTE:** Calling this endpoint requires an [admin API key](../admin-api-keys). * * This enables organization owners to share fine-tuned models with other projects in their organization. @@ -1669,6 +1719,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete checkpoint permission + * * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). * * Organization owners can use this endpoint to delete a permission for a fine-tuned model checkpoint. @@ -1695,6 +1746,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning jobs + * * List your organization's fine-tuning jobs * */ @@ -1719,6 +1771,7 @@ export class OpenAi extends _HeyApiClient { /** * Create fine-tuning job + * * Creates a fine-tuning job which begins the process of creating a new model from a given dataset. * * Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete. @@ -1751,6 +1804,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve fine-tuning job + * * Get info about a fine-tuning job. * * [Learn more about fine-tuning](https://platform.openai.com/docs/guides/model-optimization) @@ -1777,6 +1831,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel fine-tuning + * * Immediately cancel a fine-tune job. * */ @@ -1801,6 +1856,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning checkpoints + * * List checkpoints for a fine-tuning job. * */ @@ -1825,6 +1881,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning events + * * Get status updates for a fine-tuning job. * */ @@ -1849,6 +1906,7 @@ export class OpenAi extends _HeyApiClient { /** * Pause fine-tuning + * * Pause a fine-tune job. * */ @@ -1873,6 +1931,7 @@ export class OpenAi extends _HeyApiClient { /** * Resume fine-tuning + * * Resume a fine-tune job. * */ @@ -1897,6 +1956,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image edit + * * Creates an edited or extended image given one or more source images and a prompt. This endpoint only supports `gpt-image-1` and `dall-e-2`. */ public createImageEdit( @@ -1925,6 +1985,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image + * * Creates an image given a prompt. [Learn more](https://platform.openai.com/docs/guides/images). * */ @@ -1953,6 +2014,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image variation + * * Creates a variation of a given image. This endpoint only supports `dall-e-2`. */ public createImageVariation( @@ -1981,6 +2043,7 @@ export class OpenAi extends _HeyApiClient { /** * List models + * * Lists the currently available models, and provides basic information about each one such as the owner and availability. */ public listModels( @@ -2004,6 +2067,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a fine-tuned model + * * Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. */ public deleteModel( @@ -2027,6 +2091,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve model + * * Retrieves a model instance, providing basic information about the model such as the owner and permissioning. */ public retrieveModel( @@ -2050,6 +2115,7 @@ export class OpenAi extends _HeyApiClient { /** * Create moderation + * * Classifies if text and/or image inputs are potentially harmful. Learn * more in the [moderation guide](https://platform.openai.com/docs/guides/moderation). * @@ -2079,6 +2145,7 @@ export class OpenAi extends _HeyApiClient { /** * List all organization and project API keys. + * * List organization API keys */ public adminApiKeysList( @@ -2102,6 +2169,7 @@ export class OpenAi extends _HeyApiClient { /** * Create admin API key + * * Create an organization admin API key */ public adminApiKeysCreate( @@ -2129,6 +2197,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete admin API key + * * Delete an organization admin API key */ public adminApiKeysDelete( @@ -2152,6 +2221,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve admin API key + * * Retrieve a single organization API key */ public adminApiKeysGet( @@ -2175,6 +2245,7 @@ export class OpenAi extends _HeyApiClient { /** * List audit logs + * * List user actions and configuration changes within this organization. */ public listAuditLogs( @@ -2198,6 +2269,7 @@ export class OpenAi extends _HeyApiClient { /** * List organization certificates + * * List uploaded certificates for this organization. */ public listOrganizationCertificates( @@ -2221,6 +2293,7 @@ export class OpenAi extends _HeyApiClient { /** * Upload certificate + * * Upload a certificate to the organization. This does **not** automatically activate the certificate. * * Organizations can upload up to 50 certificates. @@ -2251,6 +2324,7 @@ export class OpenAi extends _HeyApiClient { /** * Activate certificates for organization + * * Activate certificates at the organization level. * * You can atomically and idempotently activate up to 10 certificates at a time. @@ -2281,6 +2355,7 @@ export class OpenAi extends _HeyApiClient { /** * Deactivate certificates for organization + * * Deactivate certificates at the organization level. * * You can atomically and idempotently deactivate up to 10 certificates at a time. @@ -2311,6 +2386,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete certificate + * * Delete a certificate from the organization. * * The certificate must be inactive for the organization and all projects. @@ -2337,6 +2413,7 @@ export class OpenAi extends _HeyApiClient { /** * Get certificate + * * Get a certificate that has been uploaded to the organization. * * You can get a certificate regardless of whether it is active or not. @@ -2363,6 +2440,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify certificate + * * Modify a certificate. Note that only the name can be modified. * */ @@ -2391,6 +2469,7 @@ export class OpenAi extends _HeyApiClient { /** * Costs + * * Get costs details for the organization. */ public usageCosts( @@ -2414,6 +2493,7 @@ export class OpenAi extends _HeyApiClient { /** * List invites + * * Returns a list of invites in the organization. */ public listInvites( @@ -2437,6 +2517,7 @@ export class OpenAi extends _HeyApiClient { /** * Create invite + * * Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization. */ public inviteUser( @@ -2464,6 +2545,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete invite + * * Delete an invite. If the invite has already been accepted, it cannot be deleted. */ public deleteInvite( @@ -2487,6 +2569,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve invite + * * Retrieves an invite. */ public retrieveInvite( @@ -2510,6 +2593,7 @@ export class OpenAi extends _HeyApiClient { /** * List projects + * * Returns a list of projects. */ public listProjects( @@ -2533,6 +2617,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project + * * Create a new project in the organization. Projects can be created and archived, but cannot be deleted. */ public createProject( @@ -2560,6 +2645,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project + * * Retrieves a project. */ public retrieveProject( @@ -2583,6 +2669,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project + * * Modifies a project in the organization. */ public modifyProject( @@ -2610,6 +2697,7 @@ export class OpenAi extends _HeyApiClient { /** * List project API keys + * * Returns a list of API keys in the project. */ public listProjectApiKeys( @@ -2633,6 +2721,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project API key + * * Deletes an API key from the project. */ public deleteProjectApiKey( @@ -2656,6 +2745,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project API key + * * Retrieves an API key in the project. */ public retrieveProjectApiKey( @@ -2679,6 +2769,7 @@ export class OpenAi extends _HeyApiClient { /** * Archive project + * * Archives a project in the organization. Archived projects cannot be used or updated. */ public archiveProject( @@ -2702,6 +2793,7 @@ export class OpenAi extends _HeyApiClient { /** * List project certificates + * * List certificates for this project. */ public listProjectCertificates( @@ -2725,6 +2817,7 @@ export class OpenAi extends _HeyApiClient { /** * Activate certificates for project + * * Activate certificates at the project level. * * You can atomically and idempotently activate up to 10 certificates at a time. @@ -2755,6 +2848,7 @@ export class OpenAi extends _HeyApiClient { /** * Deactivate certificates for project + * * Deactivate certificates at the project level. You can atomically and * idempotently deactivate up to 10 certificates at a time. * @@ -2784,6 +2878,7 @@ export class OpenAi extends _HeyApiClient { /** * List project rate limits + * * Returns the rate limits per model for a project. */ public listProjectRateLimits( @@ -2807,6 +2902,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project rate limit + * * Updates a project rate limit. */ public updateProjectRateLimits( @@ -2834,6 +2930,7 @@ export class OpenAi extends _HeyApiClient { /** * List project service accounts + * * Returns a list of service accounts in the project. */ public listProjectServiceAccounts( @@ -2857,6 +2954,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project service account + * * Creates a new service account in the project. This also returns an unredacted API key for the service account. */ public createProjectServiceAccount( @@ -2884,6 +2982,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project service account + * * Deletes a service account from the project. */ public deleteProjectServiceAccount( @@ -2907,6 +3006,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project service account + * * Retrieves a service account in the project. */ public retrieveProjectServiceAccount( @@ -2930,6 +3030,7 @@ export class OpenAi extends _HeyApiClient { /** * List project users + * * Returns a list of users in the project. */ public listProjectUsers( @@ -2953,6 +3054,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project user + * * Adds a user to the project. Users must already be members of the organization to be added to a project. */ public createProjectUser( @@ -2980,6 +3082,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project user + * * Deletes a user from the project. */ public deleteProjectUser( @@ -3003,6 +3106,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project user + * * Retrieves a user in the project. */ public retrieveProjectUser( @@ -3026,6 +3130,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project user + * * Modifies a user's role in the project. */ public modifyProjectUser( @@ -3053,6 +3158,7 @@ export class OpenAi extends _HeyApiClient { /** * Audio speeches + * * Get audio speeches usage details for the organization. */ public usageAudioSpeeches( @@ -3076,6 +3182,7 @@ export class OpenAi extends _HeyApiClient { /** * Audio transcriptions + * * Get audio transcriptions usage details for the organization. */ public usageAudioTranscriptions( @@ -3099,6 +3206,7 @@ export class OpenAi extends _HeyApiClient { /** * Code interpreter sessions + * * Get code interpreter sessions usage details for the organization. */ public usageCodeInterpreterSessions( @@ -3122,6 +3230,7 @@ export class OpenAi extends _HeyApiClient { /** * Completions + * * Get completions usage details for the organization. */ public usageCompletions( @@ -3145,6 +3254,7 @@ export class OpenAi extends _HeyApiClient { /** * Embeddings + * * Get embeddings usage details for the organization. */ public usageEmbeddings( @@ -3168,6 +3278,7 @@ export class OpenAi extends _HeyApiClient { /** * Images + * * Get images usage details for the organization. */ public usageImages( @@ -3191,6 +3302,7 @@ export class OpenAi extends _HeyApiClient { /** * Moderations + * * Get moderations usage details for the organization. */ public usageModerations( @@ -3214,6 +3326,7 @@ export class OpenAi extends _HeyApiClient { /** * Vector stores + * * Get vector stores usage details for the organization. */ public usageVectorStores( @@ -3237,6 +3350,7 @@ export class OpenAi extends _HeyApiClient { /** * List users + * * Lists all of the users in the organization. */ public listUsers( @@ -3260,6 +3374,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete user + * * Deletes a user from the organization. */ public deleteUser( @@ -3283,6 +3398,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve user + * * Retrieves a user by their identifier. */ public retrieveUser( @@ -3306,6 +3422,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify user + * * Modifies a user's role in the organization. */ public modifyUser( @@ -3333,6 +3450,7 @@ export class OpenAi extends _HeyApiClient { /** * Create session + * * Create an ephemeral API token for use in client-side applications with the * Realtime API. Can be configured with the same session parameters as the * `session.update` client event. @@ -3367,6 +3485,7 @@ export class OpenAi extends _HeyApiClient { /** * Create transcription session + * * Create an ephemeral API token for use in client-side applications with the * Realtime API specifically for realtime transcriptions. * Can be configured with the same session parameters as the `transcription_session.update` client event. @@ -3401,6 +3520,7 @@ export class OpenAi extends _HeyApiClient { /** * Create a model response + * * Creates a model response. Provide [text](https://platform.openai.com/docs/guides/text) or * [image](https://platform.openai.com/docs/guides/images) inputs to generate [text](https://platform.openai.com/docs/guides/text) * or [JSON](https://platform.openai.com/docs/guides/structured-outputs) outputs. Have the model call @@ -3435,6 +3555,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a model response + * * Deletes a model response with the given ID. * */ @@ -3459,6 +3580,7 @@ export class OpenAi extends _HeyApiClient { /** * Get a model response + * * Retrieves a model response with the given ID. * */ @@ -3483,6 +3605,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel a response + * * Cancels a model response with the given ID. Only responses created with * the `background` parameter set to `true` can be cancelled. * [Learn more](https://platform.openai.com/docs/guides/background). @@ -3509,6 +3632,7 @@ export class OpenAi extends _HeyApiClient { /** * List input items + * * Returns a list of input items for a given response. */ public listInputItems( @@ -3532,6 +3656,7 @@ export class OpenAi extends _HeyApiClient { /** * Create thread + * * Create a thread. */ public createThread( @@ -3559,6 +3684,7 @@ export class OpenAi extends _HeyApiClient { /** * Create thread and run + * * Create a thread and run it in one request. */ public createThreadAndRun( @@ -3586,6 +3712,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete thread + * * Delete a thread. */ public deleteThread( @@ -3609,6 +3736,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve thread + * * Retrieves a thread. */ public getThread( @@ -3632,6 +3760,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify thread + * * Modifies a thread. */ public modifyThread( @@ -3659,6 +3788,7 @@ export class OpenAi extends _HeyApiClient { /** * List messages + * * Returns a list of messages for a given thread. */ public listMessages( @@ -3682,6 +3812,7 @@ export class OpenAi extends _HeyApiClient { /** * Create message + * * Create a message. */ public createMessage( @@ -3709,6 +3840,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete message + * * Deletes a message. */ public deleteMessage( @@ -3732,6 +3864,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve message + * * Retrieve a message. */ public getMessage( @@ -3755,6 +3888,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify message + * * Modifies a message. */ public modifyMessage( @@ -3782,6 +3916,7 @@ export class OpenAi extends _HeyApiClient { /** * List runs + * * Returns a list of runs belonging to a thread. */ public listRuns( @@ -3805,6 +3940,7 @@ export class OpenAi extends _HeyApiClient { /** * Create run + * * Create a run. */ public createRun( @@ -3832,6 +3968,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve run + * * Retrieves a run. */ public getRun( @@ -3855,6 +3992,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify run + * * Modifies a run. */ public modifyRun( @@ -3882,6 +4020,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel a run + * * Cancels a run that is `in_progress`. */ public cancelRun( @@ -3905,6 +4044,7 @@ export class OpenAi extends _HeyApiClient { /** * List run steps + * * Returns a list of run steps belonging to a run. */ public listRunSteps( @@ -3928,6 +4068,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve run step + * * Retrieves a run step. */ public getRunStep( @@ -3951,6 +4092,7 @@ export class OpenAi extends _HeyApiClient { /** * Submit tool outputs to run + * * When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. * */ @@ -3979,6 +4121,7 @@ export class OpenAi extends _HeyApiClient { /** * Create upload + * * Creates an intermediate [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object * that you can add [Parts](https://platform.openai.com/docs/api-reference/uploads/part-object) to. * Currently, an Upload can accept at most 8 GB in total and expires after an @@ -4023,6 +4166,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel upload + * * Cancels the Upload. No Parts may be added after an Upload is cancelled. * */ @@ -4047,6 +4191,7 @@ export class OpenAi extends _HeyApiClient { /** * Complete upload + * * Completes the [Upload](https://platform.openai.com/docs/api-reference/uploads/object). * * Within the returned Upload object, there is a nested [File](https://platform.openai.com/docs/api-reference/files/object) object that is ready to use in the rest of the platform. @@ -4081,6 +4226,7 @@ export class OpenAi extends _HeyApiClient { /** * Add upload part + * * Adds a [Part](https://platform.openai.com/docs/api-reference/uploads/part-object) to an [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object. A Part represents a chunk of bytes from the file you are trying to upload. * * Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8 GB. @@ -4114,6 +4260,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector stores + * * Returns a list of vector stores. */ public listVectorStores( @@ -4137,6 +4284,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store + * * Create a vector store. */ public createVectorStore( @@ -4164,6 +4312,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete vector store + * * Delete a vector store. */ public deleteVectorStore( @@ -4187,6 +4336,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store + * * Retrieves a vector store. */ public getVectorStore( @@ -4210,6 +4360,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify vector store + * * Modifies a vector store. */ public modifyVectorStore( @@ -4237,6 +4388,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store file batch + * * Create a vector store file batch. */ public createVectorStoreFileBatch( @@ -4264,6 +4416,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file batch + * * Retrieves a vector store file batch. */ public getVectorStoreFileBatch( @@ -4287,6 +4440,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel vector store file batch + * * Cancel a vector store file batch. This attempts to cancel the processing of files in this batch as soon as possible. */ public cancelVectorStoreFileBatch( @@ -4310,6 +4464,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector store files in a batch + * * Returns a list of vector store files in a batch. */ public listFilesInVectorStoreBatch( @@ -4333,6 +4488,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector store files + * * Returns a list of vector store files. */ public listVectorStoreFiles( @@ -4356,6 +4512,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store file + * * Create a vector store file by attaching a [File](https://platform.openai.com/docs/api-reference/files) to a [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object). */ public createVectorStoreFile( @@ -4383,6 +4540,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete vector store file + * * Delete a vector store file. This will remove the file from the vector store but the file itself will not be deleted. To delete the file, use the [delete file](https://platform.openai.com/docs/api-reference/files/delete) endpoint. */ public deleteVectorStoreFile( @@ -4406,6 +4564,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file + * * Retrieves a vector store file. */ public getVectorStoreFile( @@ -4429,6 +4588,7 @@ export class OpenAi extends _HeyApiClient { /** * Update vector store file attributes + * * Update attributes on a vector store file. */ public updateVectorStoreFileAttributes( @@ -4456,6 +4616,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file content + * * Retrieve the parsed contents of a vector store file. */ public retrieveVectorStoreFileContent( @@ -4479,6 +4640,7 @@ export class OpenAi extends _HeyApiClient { /** * Search vector store + * * Search a vector store for relevant chunks based on a query and file attributes filter. */ public searchVectorStore( diff --git a/examples/openapi-ts-openai/src/client/types.gen.ts b/examples/openapi-ts-openai/src/client/types.gen.ts index 78630ab51..212e77a5e 100644 --- a/examples/openapi-ts-openai/src/client/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/types.gen.ts @@ -1,5 +1,25 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://api.openai.com/v1' | (string & {}); +}; + +export type Webhooks = + | PostBatchCancelledWebhookRequest + | PostBatchCompletedWebhookRequest + | PostBatchExpiredWebhookRequest + | PostBatchFailedWebhookRequest + | PostEvalRunCanceledWebhookRequest + | PostEvalRunFailedWebhookRequest + | PostEvalRunSucceededWebhookRequest + | PostFineTuningJobCancelledWebhookRequest + | PostFineTuningJobFailedWebhookRequest + | PostFineTuningJobSucceededWebhookRequest + | PostResponseCancelledWebhookRequest + | PostResponseCompletedWebhookRequest + | PostResponseFailedWebhookRequest + | PostResponseIncompleteWebhookRequest; + export type AddUploadPartRequest = { /** * The chunk of bytes for this Part. @@ -78,6 +98,7 @@ export type ApiKeyList = { /** * Assistant + * * Represents an `assistant` that can call the model and use tools. */ export type AssistantObject = { @@ -194,44 +215,44 @@ export type AssistantStreamEvent = } & ErrorEvent); export const AssistantSupportedModels = { - GPT_3_5_TURBO: 'gpt-3.5-turbo', - GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', - GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', - GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', - GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', - GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', - GPT_4: 'gpt-4', GPT_4O: 'gpt-4o', + GPT_4: 'gpt-4', GPT_4O_2024_05_13: 'gpt-4o-2024-05-13', GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', GPT_4O_2024_11_20: 'gpt-4o-2024-11-20', GPT_4O_MINI: 'gpt-4o-mini', GPT_4O_MINI_2024_07_18: 'gpt-4o-mini-2024-07-18', + GPT_3_5_TURBO: 'gpt-3.5-turbo', GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', + GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', GPT_4_0314: 'gpt-4-0314', + GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', GPT_4_0613: 'gpt-4-0613', + GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', GPT_4_1: 'gpt-4.1', - GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', + GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', + GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', GPT_4_1_MINI: 'gpt-4.1-mini', + GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', GPT_4_1_NANO: 'gpt-4.1-nano', GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', GPT_4_32K: 'gpt-4-32k', + GPT_5: 'gpt-5', GPT_4_32K_0314: 'gpt-4-32k-0314', + GPT_5_2025_08_07: 'gpt-5-2025-08-07', GPT_4_32K_0613: 'gpt-4-32k-0613', + GPT_5_MINI: 'gpt-5-mini', GPT_4_5_PREVIEW: 'gpt-4.5-preview', + GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', GPT_4_5_PREVIEW_2025_02_27: 'gpt-4.5-preview-2025-02-27', + GPT_5_NANO: 'gpt-5-nano', GPT_4_TURBO: 'gpt-4-turbo', GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', + GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', - GPT_5: 'gpt-5', - GPT_5_2025_08_07: 'gpt-5-2025-08-07', - GPT_5_MINI: 'gpt-5-mini', - GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', - GPT_5_NANO: 'gpt-5-nano', - GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', O1: 'o1', O1_2024_12_17: 'o1-2024-12-17', O3_MINI: 'o3-mini', @@ -320,7 +341,9 @@ export type AssistantsApiResponseFormatOption = * */ export type AssistantsApiToolChoiceOption = - | ('none' | 'auto' | 'required') + | 'none' + | 'auto' + | 'required' | AssistantsNamedToolChoice; /** @@ -931,6 +954,7 @@ export type AuditLogEventType = /** * Auto Chunking Strategy + * * The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. */ export type AutoChunkingStrategyRequestParam = { @@ -1027,6 +1051,7 @@ export type Batch = { /** * File expiration policy + * * The expiration policy for the output and/or error file that are generated for a batch. */ export type BatchFileExpirationAfter = { @@ -1148,6 +1173,7 @@ export type Certificate = { /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -1181,6 +1207,7 @@ export type ChatCompletionAllowedTools = { /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -1235,6 +1262,7 @@ export type ChatCompletionFunctions = { /** * ChatCompletionList + * * An object representing a list of Chat Completions. * */ @@ -1265,6 +1293,7 @@ export type ChatCompletionList = { /** * Custom tool call + * * A call to a custom tool created by the model. * */ @@ -1294,6 +1323,7 @@ export type ChatCompletionMessageCustomToolCall = { /** * ChatCompletionMessageList + * * An object representing a list of chat completion messages. * */ @@ -1340,6 +1370,7 @@ export type ChatCompletionMessageList = { /** * Function tool call + * * A call to a function tool created by the model. * */ @@ -1418,6 +1449,7 @@ export type ChatCompletionModalities = Array<'text' | 'audio'>; /** * Function tool choice + * * Specifies a tool the model should use. Use to force the model to call a specific function. */ export type ChatCompletionNamedToolChoice = { @@ -1435,6 +1467,7 @@ export type ChatCompletionNamedToolChoice = { /** * Custom tool choice + * * Specifies a tool the model should use. Use to force the model to call a specific custom tool. */ export type ChatCompletionNamedToolChoiceCustom = { @@ -1452,6 +1485,7 @@ export type ChatCompletionNamedToolChoiceCustom = { /** * Assistant message + * * Messages sent by the model in response to user messages. * */ @@ -1475,6 +1509,7 @@ export type ChatCompletionRequestAssistantMessage = { content?: string | Array; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1512,6 +1547,7 @@ export type ChatCompletionRequestAssistantMessageContentPart = /** * Developer message + * * Developer-provided instructions that the model should follow, regardless of * messages sent by the user. With o1 models and newer, `developer` messages * replace the previous `system` messages. @@ -1534,6 +1570,7 @@ export type ChatCompletionRequestDeveloperMessage = { /** * Function message + * * @deprecated */ export type ChatCompletionRequestFunctionMessage = { @@ -1573,6 +1610,7 @@ export type ChatCompletionRequestMessage = /** * Audio content part + * * Learn about [audio inputs](https://platform.openai.com/docs/guides/audio). * */ @@ -1596,6 +1634,7 @@ export type ChatCompletionRequestMessageContentPartAudio = { /** * File content part + * * Learn about [file inputs](https://platform.openai.com/docs/guides/text) for text generation. * */ @@ -1627,6 +1666,7 @@ export type ChatCompletionRequestMessageContentPartFile = { /** * Image content part + * * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). * */ @@ -1663,6 +1703,7 @@ export type ChatCompletionRequestMessageContentPartRefusal = { /** * Text content part + * * Learn about [text inputs](https://platform.openai.com/docs/guides/text-generation). * */ @@ -1679,6 +1720,7 @@ export type ChatCompletionRequestMessageContentPartText = { /** * System message + * * Developer-provided instructions that the model should follow, regardless of * messages sent by the user. With o1 models and newer, use `developer` messages * for this purpose instead. @@ -1725,6 +1767,7 @@ export type ChatCompletionRequestToolMessageContentPart = /** * User message + * * Messages sent by an end user, containing prompts or additional context * information. * @@ -1829,6 +1872,7 @@ export type ChatCompletionResponseMessage = { content: string; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1910,6 +1954,7 @@ export type ChatCompletionStreamResponseDelta = { content?: string; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1967,6 +2012,7 @@ export type ChatCompletionTokenLogprob = { /** * Function tool + * * A function tool that can be used to generate a response. * */ @@ -1989,7 +2035,9 @@ export type ChatCompletionTool = { * */ export type ChatCompletionToolChoiceOption = - | ('none' | 'auto' | 'required') + | 'none' + | 'auto' + | 'required' | ChatCompletionAllowedToolsChoice | ChatCompletionNamedToolChoice | ChatCompletionNamedToolChoiceCustom; @@ -2007,6 +2055,7 @@ export type ChunkingStrategyRequestParam = /** * Click + * * A click action. * */ @@ -2036,6 +2085,7 @@ export type Click = { /** * Code interpreter file output + * * The output of a code interpreter tool call that is a file. * */ @@ -2061,6 +2111,7 @@ export type CodeInterpreterFileOutput = { /** * Code interpreter output image + * * The image output from the code interpreter. * */ @@ -2077,6 +2128,7 @@ export type CodeInterpreterOutputImage = { /** * Code interpreter output logs + * * The logs output from the code interpreter. * */ @@ -2093,6 +2145,7 @@ export type CodeInterpreterOutputLogs = { /** * Code interpreter text output + * * The output of a code interpreter tool call that is text. * */ @@ -2111,6 +2164,7 @@ export type CodeInterpreterTextOutput = { /** * Code interpreter + * * A tool that runs Python code to help generate a response to a prompt. * */ @@ -2130,6 +2184,7 @@ export type CodeInterpreterTool = { /** * CodeInterpreterContainerAuto + * * Configuration for a code interpreter container. Optionally specify the IDs * of the files to run the code on. * @@ -2148,6 +2203,7 @@ export type CodeInterpreterToolAuto = { /** * Code interpreter tool call + * * A tool call to run code. * */ @@ -2199,6 +2255,7 @@ export type CodeInterpreterToolCall = { /** * Comparison Filter + * * A filter used to compare a specified attribute key to a given value using a defined comparison operation. * */ @@ -2298,6 +2355,7 @@ export type CompletionUsage = { /** * Compound Filter + * * Combine multiple filters using `and` or `or`. */ export type CompoundFilter = { @@ -2363,6 +2421,7 @@ export type ComputerScreenshotImage = { /** * Computer tool call + * * A tool call to a computer use tool. See the * [computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information. * @@ -2397,6 +2456,7 @@ export type ComputerToolCall = { /** * Computer tool call output + * * The output of a computer tool call. * */ @@ -2588,6 +2648,7 @@ export type Content = InputContent | OutputContent; /** * Coordinate + * * An x/y coordinate pair, e.g. `{ x: 100, y: 200 }`. * */ @@ -2780,14 +2841,16 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * `none` is the default when no functions are present. `auto` is the default * if functions are present. * + * * @deprecated */ - function_call?: ('none' | 'auto') | ChatCompletionFunctionCallOption; + function_call?: 'none' | 'auto' | ChatCompletionFunctionCallOption; /** * Deprecated in favor of `tools`. * * A list of functions the model may generate JSON inputs for. * + * * @deprecated */ functions?: Array; @@ -2825,6 +2888,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * This value is now deprecated in favor of `max_completion_tokens`, and is * not compatible with [o-series models](https://platform.openai.com/docs/guides/reasoning). * + * * @deprecated */ max_tokens?: number; @@ -2890,6 +2954,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result. * Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend. * + * * @deprecated */ seed?: number; @@ -2938,6 +3003,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { verbosity?: Verbosity; /** * Web search + * * This tool searches the web for relevant results to use in a response. * Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat). * @@ -3021,6 +3087,7 @@ export type CreateChatCompletionResponse = { * * Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. * + * * @deprecated */ system_fingerprint?: string; @@ -3093,6 +3160,7 @@ export type CreateChatCompletionStreamResponse = { * This fingerprint represents the backend configuration that the model runs with. * Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. * + * * @deprecated */ system_fingerprint?: string; @@ -3161,7 +3229,7 @@ export type CreateCompletionRequest = { * ID of the model to use. You can use the [List models](https://platform.openai.com/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](https://platform.openai.com/docs/models) for descriptions of them. * */ - model: string | ('gpt-3.5-turbo-instruct' | 'davinci-002' | 'babbage-002'); + model: string | 'gpt-3.5-turbo-instruct' | 'davinci-002' | 'babbage-002'; /** * How many completions to generate for each prompt. * @@ -3332,11 +3400,9 @@ export type CreateEmbeddingRequest = { */ model: | string - | ( - | 'text-embedding-ada-002' - | 'text-embedding-3-small' - | 'text-embedding-3-large' - ); + | 'text-embedding-ada-002' + | 'text-embedding-3-small' + | 'text-embedding-3-large'; /** * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). * @@ -3374,6 +3440,7 @@ export type CreateEmbeddingResponse = { /** * CompletionsRunDataSource + * * A CompletionsRunDataSource object describing a model sampling configuration. * */ @@ -3474,6 +3541,7 @@ export type CreateEvalCompletionsRunDataSource = { /** * CustomDataSourceConfig + * * A CustomDataSourceConfig object that defines the schema for the data source used for the evaluation runs. * This schema is used to define the shape of the data that will be: * - Used to define your testing criteria and @@ -3499,6 +3567,7 @@ export type CreateEvalCustomDataSourceConfig = { /** * CreateEvalItem + * * A chat message that makes up the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. */ export type CreateEvalItem = @@ -3516,6 +3585,7 @@ export type CreateEvalItem = /** * JsonlRunDataSource + * * A JsonlRunDataSource object with that specifies a JSONL file that matches the eval * */ @@ -3538,6 +3608,7 @@ export type CreateEvalJsonlRunDataSource = { /** * LabelModelGrader + * * A LabelModelGrader object which uses a model to assign labels to each item * in the evaluation. * @@ -3571,6 +3642,7 @@ export type CreateEvalLabelModelGrader = { /** * LogsDataSourceConfig + * * A data source config which specifies the metadata property of your logs query. * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. * @@ -3634,6 +3706,7 @@ export type CreateEvalRequest = { /** * ResponsesRunDataSource + * * A ResponsesRunDataSource object describing a model sampling configuration. * */ @@ -3761,8 +3834,10 @@ export type CreateEvalRunRequest = { /** * StoredCompletionsDataSourceConfig + * * Deprecated in favor of LogsDataSourceConfig. * + * * @deprecated */ export type CreateEvalStoredCompletionsDataSourceConfig = { @@ -3800,6 +3875,7 @@ export type CreateFineTuningJobRequest = { * The hyperparameters used for the fine-tuning job. * This value is now deprecated in favor of `method`, and should be passed in under the `method` parameter. * + * * @deprecated */ hyperparameters?: { @@ -3871,7 +3947,10 @@ export type CreateFineTuningJobRequest = { */ model: | string - | ('babbage-002' | 'davinci-002' | 'gpt-3.5-turbo' | 'gpt-4o-mini'); + | 'babbage-002' + | 'davinci-002' + | 'gpt-3.5-turbo' + | 'gpt-4o-mini'; /** * The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases. * If a seed is not specified, one will be generated for you. @@ -3936,7 +4015,7 @@ export type CreateImageEditRequest = { * `png` file less than 4MB. * */ - image: (Blob | File) | Array; + image: Blob | File | Array; input_fidelity?: ImageInputFidelity; /** * An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where `image` should be edited. If there are multiple images provided, the mask will be applied on the first image. Must be a valid PNG file, less than 4MB, and have the same dimensions as `image`. @@ -3945,7 +4024,7 @@ export type CreateImageEditRequest = { /** * The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are supported. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. */ - model?: string | ('dall-e-2' | 'gpt-image-1'); + model?: string | 'dall-e-2' | 'gpt-image-1'; /** * The number of images to generate. Must be between 1 and 10. */ @@ -4016,7 +4095,7 @@ export type CreateImageRequest = { /** * The model to use for image generation. One of `dall-e-2`, `dall-e-3`, or `gpt-image-1`. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. */ - model?: string | ('dall-e-2' | 'dall-e-3' | 'gpt-image-1'); + model?: string | 'dall-e-2' | 'dall-e-3' | 'gpt-image-1'; /** * Control the content-moderation level for images generated by `gpt-image-1`. Must be either `low` for less restrictive filtering or `auto` (default value). */ @@ -4188,12 +4267,10 @@ export type CreateModerationRequest = { */ model?: | string - | ( - | 'omni-moderation-latest' - | 'omni-moderation-2024-09-26' - | 'text-moderation-latest' - | 'text-moderation-stable' - ); + | 'omni-moderation-latest' + | 'omni-moderation-2024-09-26' + | 'text-moderation-latest' + | 'text-moderation-stable'; }; /** @@ -4526,7 +4603,7 @@ export type CreateSpeechRequest = { * One of the available [TTS models](https://platform.openai.com/docs/models#tts): `tts-1`, `tts-1-hd` or `gpt-4o-mini-tts`. * */ - model: string | ('tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts'); + model: string | 'tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts'; /** * The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. */ @@ -4578,46 +4655,44 @@ export type CreateThreadAndRunRequest = { */ model?: | string - | ( - | 'gpt-5' - | 'gpt-5-mini' - | 'gpt-5-nano' - | 'gpt-5-2025-08-07' - | 'gpt-5-mini-2025-08-07' - | 'gpt-5-nano-2025-08-07' - | 'gpt-4.1' - | 'gpt-4.1-mini' - | 'gpt-4.1-nano' - | 'gpt-4.1-2025-04-14' - | 'gpt-4.1-mini-2025-04-14' - | 'gpt-4.1-nano-2025-04-14' - | 'gpt-4o' - | 'gpt-4o-2024-11-20' - | 'gpt-4o-2024-08-06' - | 'gpt-4o-2024-05-13' - | 'gpt-4o-mini' - | 'gpt-4o-mini-2024-07-18' - | 'gpt-4.5-preview' - | 'gpt-4.5-preview-2025-02-27' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - ); + | 'gpt-5' + | 'gpt-5-mini' + | 'gpt-5-nano' + | 'gpt-5-2025-08-07' + | 'gpt-5-mini-2025-08-07' + | 'gpt-5-nano-2025-08-07' + | 'gpt-4.1' + | 'gpt-4.1-mini' + | 'gpt-4.1-nano' + | 'gpt-4.1-2025-04-14' + | 'gpt-4.1-mini-2025-04-14' + | 'gpt-4.1-nano-2025-04-14' + | 'gpt-4o' + | 'gpt-4o-2024-11-20' + | 'gpt-4o-2024-08-06' + | 'gpt-4o-2024-05-13' + | 'gpt-4o-mini' + | 'gpt-4o-mini-2024-07-18' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' + | 'gpt-4-turbo' + | 'gpt-4-turbo-2024-04-09' + | 'gpt-4-0125-preview' + | 'gpt-4-turbo-preview' + | 'gpt-4-1106-preview' + | 'gpt-4-vision-preview' + | 'gpt-4' + | 'gpt-4-0314' + | 'gpt-4-0613' + | 'gpt-4-32k' + | 'gpt-4-32k-0314' + | 'gpt-4-32k-0613' + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-0613' + | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-0125' + | 'gpt-3.5-turbo-16k-0613'; parallel_tool_calls?: ParallelToolCalls; response_format?: AssistantsApiResponseFormatOption; /** @@ -4765,9 +4840,7 @@ export type CreateTranscriptionRequest = { * ID of the model to use. The options are `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, and `whisper-1` (which is powered by our open source Whisper V2 model). * */ - model: - | string - | ('whisper-1' | 'gpt-4o-transcribe' | 'gpt-4o-mini-transcribe'); + model: string | 'whisper-1' | 'gpt-4o-transcribe' | 'gpt-4o-mini-transcribe'; /** * An optional text to guide the model's style or continue a previous audio segment. The [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting) should match the audio language. * @@ -4982,6 +5055,7 @@ export type CreateVectorStoreRequest = { /** * Custom tool + * * A custom tool that processes input using a specified format. Learn more about * [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools). * @@ -5029,6 +5103,7 @@ export type CustomTool = { /** * Custom tool call + * * A call to a custom tool created by the model. * */ @@ -5062,6 +5137,7 @@ export type CustomToolCall = { /** * Custom tool call output + * * The output of a custom tool call from your code, being sent back to the model. * */ @@ -5090,12 +5166,14 @@ export type CustomToolCallOutput = { /** * Custom tool + * * A custom tool that processes input using a specified format. * */ export type CustomToolChatCompletions = { /** * Custom tool properties + * * Properties of the custom tool. * */ @@ -5119,6 +5197,7 @@ export type CustomToolChatCompletions = { | { /** * Grammar format + * * Your chosen grammar. */ grammar: { @@ -5225,6 +5304,7 @@ export type DoneEvent = { /** * DoubleClick + * * A double click action. * */ @@ -5249,6 +5329,7 @@ export type DoubleClick = { /** * Drag + * * A drag action. * */ @@ -5275,6 +5356,7 @@ export type Drag = { /** * Input message + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. Messages with the @@ -5343,6 +5425,7 @@ export type ErrorResponse = { /** * Eval + * * An Eval object with a data source config and testing criteria. * An Eval represents a task to be done for your LLM integration. * Like: @@ -5396,6 +5479,7 @@ export type Eval = { /** * EvalApiError + * * An object representing an error response from the Eval API. * */ @@ -5412,6 +5496,7 @@ export type EvalApiError = { /** * CustomDataSourceConfig + * * A CustomDataSourceConfig which specifies the schema of your `item` and optionally `sample` namespaces. * The response schema defines the shape of the data that will be: * - Used to define your testing criteria and @@ -5475,6 +5560,7 @@ export type EvalGraderTextSimilarity = GraderTextSimilarity & { /** * Eval message object + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. Messages with the @@ -5570,6 +5656,7 @@ export type EvalJsonlFileIdSource = { /** * EvalList + * * An object representing a list of evals. * */ @@ -5600,6 +5687,7 @@ export type EvalList = { /** * LogsDataSourceConfig + * * A LogsDataSourceConfig which specifies the metadata property of your logs query. * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. * The schema returned by this data source config is used to defined what variables are available in your evals. @@ -5624,6 +5712,7 @@ export type EvalLogsDataSourceConfig = { /** * EvalResponsesSource + * * A EvalResponsesSource object describing a run data source configuration. * */ @@ -5678,6 +5767,7 @@ export type EvalResponsesSource = { /** * EvalRun + * * A schema representing an evaluation run. * */ @@ -5800,6 +5890,7 @@ export type EvalRun = { /** * EvalRunList + * * An object representing a list of runs for an evaluation. * */ @@ -5830,6 +5921,7 @@ export type EvalRunList = { /** * EvalRunOutputItem + * * A schema representing an evaluation run output item. * */ @@ -5955,6 +6047,7 @@ export type EvalRunOutputItem = { /** * EvalRunOutputItemList + * * An object representing a list of output items for an evaluation run. * */ @@ -5985,8 +6078,10 @@ export type EvalRunOutputItemList = { /** * StoredCompletionsDataSourceConfig + * * Deprecated in favor of LogsDataSourceConfig. * + * * @deprecated */ export type EvalStoredCompletionsDataSourceConfig = { @@ -6007,6 +6102,7 @@ export type EvalStoredCompletionsDataSourceConfig = { /** * StoredCompletionsRunDataSource + * * A StoredCompletionsRunDataSource configuration describing a set of filters * */ @@ -6036,6 +6132,7 @@ export type EvalStoredCompletionsSource = { /** * File expiration policy + * * The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. */ export type FileExpirationAfter = { @@ -6051,6 +6148,7 @@ export type FileExpirationAfter = { /** * File path + * * A path to a file. * */ @@ -6088,6 +6186,7 @@ export type FileSearchRanker = /** * File search tool call ranking options + * * The ranking options for the file search. If not specified, the file search tool will use the `auto` ranker and a score_threshold of 0. * * See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search#customizing-file-search-settings) for more information. @@ -6103,6 +6202,7 @@ export type FileSearchRankingOptions = { /** * File search tool call + * * The results of a file search tool call. See the * [file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information. * @@ -6174,6 +6274,7 @@ export type FineTuneChatCompletionRequestAssistantMessage = { export type FineTuneChatRequestInput = { /** * A list of functions the model may generate JSON inputs for. + * * @deprecated */ functions?: Array; @@ -6375,6 +6476,7 @@ export type FineTuneSupervisedMethod = { /** * FineTuningCheckpointPermission + * * The `checkpoint.permission` object represents a permission for a fine-tuned model checkpoint. * */ @@ -6439,6 +6541,7 @@ export type FineTuningIntegration = { /** * FineTuningJob + * * The `fine_tuning.job` object represents a fine-tuning job that has been created through the API. * */ @@ -6559,6 +6662,7 @@ export type FineTuningJob = { /** * FineTuningJobCheckpoint + * * The `fine_tuning.job.checkpoint` object represents a model checkpoint for a fine-tuning job that is ready to use. * */ @@ -6664,6 +6768,7 @@ export type FunctionParameters = { /** * Function tool call + * * A tool call to run a function. See the * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. * @@ -6704,6 +6809,7 @@ export type FunctionToolCall = { /** * Function tool call output + * * The output of a function tool call. * */ @@ -6755,6 +6861,7 @@ export type FunctionToolCallResource = FunctionToolCall & { /** * LabelModelGrader + * * A LabelModelGrader object which uses a model to assign labels to each item * in the evaluation. * @@ -6785,6 +6892,7 @@ export type GraderLabelModel = { /** * MultiGrader + * * A MultiGrader object combines the output of multiple graders to produce a single score. */ export type GraderMulti = { @@ -6810,6 +6918,7 @@ export type GraderMulti = { /** * PythonGrader + * * A PythonGrader object that runs a python script on the input. * */ @@ -6834,6 +6943,7 @@ export type GraderPython = { /** * ScoreModelGrader + * * A ScoreModelGrader object that uses a model to assign a score to the input. * */ @@ -6868,6 +6978,7 @@ export type GraderScoreModel = { /** * StringCheckGrader + * * A StringCheckGrader object that performs a string comparison between input and reference using a specified operation. * */ @@ -6896,6 +7007,7 @@ export type GraderStringCheck = { /** * TextSimilarityGrader + * * A TextSimilarityGrader object which grades text based on similarity metrics. * */ @@ -7152,6 +7264,7 @@ export type ImageGenStreamEvent = /** * Image generation tool + * * A tool that generates images using a model like `gpt-image-1`. * */ @@ -7227,6 +7340,7 @@ export type ImageGenTool = { /** * Image generation call + * * An image generation request made by the model. * */ @@ -7275,6 +7389,7 @@ export type ImageInputFidelity = /** * Image generation response + * * The response from the image generation endpoint. */ export type ImagesResponse = { @@ -7386,6 +7501,7 @@ export type Includable = (typeof Includable)[keyof typeof Includable]; /** * Audio input + * * An audio input to the model. * */ @@ -7432,6 +7548,7 @@ export type InputItem = /** * Input message + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. @@ -7459,6 +7576,7 @@ export type InputMessage = { /** * Input item content list + * * A list of one or many input items to the model, containing different content * types. * @@ -7696,6 +7814,7 @@ export type ItemResource = /** * KeyPress + * * A collection of keypresses the model would like to perform. * */ @@ -7829,6 +7948,7 @@ export type ListVectorStoresResponse = { /** * Local shell exec action + * * Execute a shell command on the server. * */ @@ -7869,6 +7989,7 @@ export type LocalShellExecAction = { /** * Local shell tool + * * A tool that allows the model to execute shell commands in a local environment. * */ @@ -7881,6 +8002,7 @@ export type LocalShellTool = { /** * Local shell call + * * A tool call to run a command on the local shell. * */ @@ -7910,6 +8032,7 @@ export type LocalShellToolCall = { /** * Local shell call output + * * The output of a local shell tool call. * */ @@ -7960,6 +8083,7 @@ export type LogProbProperties = { /** * MCP approval request + * * A request for human approval of a tool invocation. * */ @@ -7993,6 +8117,7 @@ export type McpApprovalRequest = { /** * MCP approval response + * * A response to an MCP approval request. * */ @@ -8026,6 +8151,7 @@ export type McpApprovalResponse = { /** * MCP approval response + * * A response to an MCP approval request. * */ @@ -8059,6 +8185,7 @@ export type McpApprovalResponseResource = { /** * MCP list tools + * * A list of tools available on an MCP server. * */ @@ -8092,6 +8219,7 @@ export type McpListTools = { /** * MCP list tools tool + * * A tool available on an MCP server. * */ @@ -8124,6 +8252,7 @@ export type McpListToolsTool = { /** * MCP tool + * * Give the model access to additional tools via remote Model Context Protocol * (MCP) servers. [Learn more about MCP](https://platform.openai.com/docs/guides/tools-remote-mcp). * @@ -8138,6 +8267,7 @@ export type McpTool = { | { /** * MCP allowed tools + * * List of allowed tool names. */ tool_names?: Array; @@ -8176,7 +8306,8 @@ export type McpTool = { tool_names?: Array; }; } - | ('always' | 'never'); + | 'always' + | 'never'; /** * Optional description of the MCP server, used to provide more context. * @@ -8200,6 +8331,7 @@ export type McpTool = { /** * MCP tool call + * * An invocation of a tool on an MCP server. * */ @@ -8243,6 +8375,7 @@ export type McpToolCall = { /** * Image file + * * References an image [File](https://platform.openai.com/docs/api-reference/files) in the content of a message. */ export type MessageContentImageFileObject = { @@ -8264,6 +8397,7 @@ export type MessageContentImageFileObject = { /** * Image URL + * * References an image URL in the content of a message. */ export type MessageContentImageUrlObject = { @@ -8285,6 +8419,7 @@ export type MessageContentImageUrlObject = { /** * Refusal + * * The refusal content generated by the assistant. */ export type MessageContentRefusalObject = { @@ -8297,6 +8432,7 @@ export type MessageContentRefusalObject = { /** * File citation + * * A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. */ export type MessageContentTextAnnotationsFileCitationObject = { @@ -8320,6 +8456,7 @@ export type MessageContentTextAnnotationsFileCitationObject = { /** * File path + * * A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. */ export type MessageContentTextAnnotationsFilePathObject = { @@ -8343,6 +8480,7 @@ export type MessageContentTextAnnotationsFilePathObject = { /** * Text + * * The text content that is part of a message. */ export type MessageContentTextObject = { @@ -8361,6 +8499,7 @@ export type MessageContentTextObject = { /** * Image file + * * References an image [File](https://platform.openai.com/docs/api-reference/files) in the content of a message. */ export type MessageDeltaContentImageFileObject = { @@ -8386,6 +8525,7 @@ export type MessageDeltaContentImageFileObject = { /** * Image URL + * * References an image URL in the content of a message. */ export type MessageDeltaContentImageUrlObject = { @@ -8411,6 +8551,7 @@ export type MessageDeltaContentImageUrlObject = { /** * Refusal + * * The refusal content that is part of a message. */ export type MessageDeltaContentRefusalObject = { @@ -8427,6 +8568,7 @@ export type MessageDeltaContentRefusalObject = { /** * File citation + * * A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. */ export type MessageDeltaContentTextAnnotationsFileCitationObject = { @@ -8458,6 +8600,7 @@ export type MessageDeltaContentTextAnnotationsFileCitationObject = { /** * File path + * * A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. */ export type MessageDeltaContentTextAnnotationsFilePathObject = { @@ -8485,6 +8628,7 @@ export type MessageDeltaContentTextAnnotationsFilePathObject = { /** * Text + * * The text content that is part of a message. */ export type MessageDeltaContentTextObject = { @@ -8507,6 +8651,7 @@ export type MessageDeltaContentTextObject = { /** * Message delta object + * * Represents a message delta i.e. any changed fields on a message during streaming. * */ @@ -8536,6 +8681,7 @@ export type MessageDeltaObject = { /** * The message object + * * Represents a message within a [thread](https://platform.openai.com/docs/api-reference/threads). */ export type MessageObject = { @@ -8615,6 +8761,7 @@ export type MessageObject = { /** * Text + * * The text content that is part of a message. */ export type MessageRequestContentTextObject = { @@ -8665,6 +8812,7 @@ export type Metadata = { /** * Model + * * Describes an OpenAI model offering that can be used with the API. */ export type Model = { @@ -8690,18 +8838,16 @@ export type ModelIds = ModelIdsShared | ModelIdsResponses; export type ModelIdsResponses = | ModelIdsShared - | ( - | 'o1-pro' - | 'o1-pro-2025-03-19' - | 'o3-pro' - | 'o3-pro-2025-06-10' - | 'o3-deep-research' - | 'o3-deep-research-2025-06-26' - | 'o4-mini-deep-research' - | 'o4-mini-deep-research-2025-06-26' - | 'computer-use-preview' - | 'computer-use-preview-2025-03-11' - ); + | 'o1-pro' + | 'o1-pro-2025-03-19' + | 'o3-pro' + | 'o3-pro-2025-06-10' + | 'o3-deep-research' + | 'o3-deep-research-2025-06-26' + | 'o4-mini-deep-research' + | 'o4-mini-deep-research-2025-06-26' + | 'computer-use-preview' + | 'computer-use-preview-2025-03-11'; export type ModelIdsShared = string | ChatModel; @@ -8746,6 +8892,7 @@ export type ModelResponseProperties = { * A stable identifier for your end-users. * Used to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). * + * * @deprecated */ user?: string; @@ -8855,6 +9002,7 @@ export type ModifyThreadRequest = { /** * Move + * * A mouse move action. * */ @@ -8879,6 +9027,7 @@ export type Move = { /** * OpenAIFile + * * The `File` object represents a document that has been uploaded to OpenAI. */ export type OpenAiFile = { @@ -8920,11 +9069,13 @@ export type OpenAiFile = { | 'user_data'; /** * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + * * @deprecated */ status: 'uploaded' | 'processed' | 'error'; /** * Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`. + * * @deprecated */ status_details?: string; @@ -8932,6 +9083,7 @@ export type OpenAiFile = { /** * Other Chunking Strategy + * * This is returned when the chunking strategy is unknown. Typically, this is because the file was indexed before the `chunking_strategy` concept was introduced in the API. */ export type OtherChunkingStrategyResponseParam = { @@ -8943,6 +9095,7 @@ export type OtherChunkingStrategyResponseParam = { /** * Output audio + * * An audio output from the model. * */ @@ -9015,6 +9168,7 @@ export type OutputItem = /** * Output message + * * An output message from the model. * */ @@ -9065,6 +9219,7 @@ export type PartialImages = number; /** * Static Content + * * Static predicted output content, such as the content of a text file that is * being regenerated. * @@ -10103,7 +10258,7 @@ export type RealtimeResponseCreateParams = { * will not add items to default conversation. * */ - conversation?: string | ('auto' | 'none'); + conversation?: string | 'auto' | 'none'; /** * Input items to include in the prompt for the model. Using this field * creates a new context for this Response instead of using the default @@ -11489,6 +11644,7 @@ export type RealtimeSession = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -11735,6 +11891,7 @@ export type RealtimeSessionCreateRequest = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -11950,6 +12107,7 @@ export type RealtimeSessionCreateResponse = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -12261,6 +12419,7 @@ export type RealtimeTranscriptionSessionCreateResponse = { /** * Reasoning + * * **gpt-5 and o-series models only** * * Configuration options for @@ -12276,6 +12435,7 @@ export type Reasoning = { * useful for debugging and understanding the model's reasoning process. * One of `auto`, `concise`, or `detailed`. * + * * @deprecated */ generate_summary?: 'auto' | 'concise' | 'detailed'; @@ -12316,6 +12476,7 @@ export type ReasoningEffort = /** * Reasoning + * * A description of the chain of thought used by a reasoning model while generating * a response. Be sure to include these items in your `input` to the Responses API * for subsequent turns of a conversation if you are manually @@ -12761,6 +12922,7 @@ export type ResponseCreatedEvent = { /** * ResponseCustomToolCallInputDelta + * * Event representing a delta (partial update) to the input of a custom tool call. * */ @@ -12789,6 +12951,7 @@ export type ResponseCustomToolCallInputDeltaEvent = { /** * ResponseCustomToolCallInputDone + * * Event indicating that input for a custom tool call is complete. * */ @@ -12988,6 +13151,7 @@ export type ResponseFileSearchCallSearchingEvent = { /** * JSON object + * * JSON object response format. An older method of generating JSON responses. * Using `json_schema` is recommended for models that support it. Note that the * model will not generate JSON without a system or user message instructing it @@ -13003,6 +13167,7 @@ export type ResponseFormatJsonObject = { /** * JSON schema + * * JSON Schema response format. Used to generate structured JSON responses. * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). * @@ -13010,6 +13175,7 @@ export type ResponseFormatJsonObject = { export type ResponseFormatJsonSchema = { /** * JSON schema + * * Structured Outputs configuration options, including a JSON Schema. * */ @@ -13045,6 +13211,7 @@ export type ResponseFormatJsonSchema = { /** * JSON schema + * * The schema for the response format, described as a JSON Schema object. * Learn how to build JSON schemas [here](https://json-schema.org/). * @@ -13055,6 +13222,7 @@ export type ResponseFormatJsonSchemaSchema = { /** * Text + * * Default response format. Used to generate text responses. * */ @@ -13067,6 +13235,7 @@ export type ResponseFormatText = { /** * Text grammar + * * A custom grammar for the model to follow when generating text. * Learn more in the [custom grammars guide](https://platform.openai.com/docs/guides/custom-grammars). * @@ -13084,6 +13253,7 @@ export type ResponseFormatTextGrammar = { /** * Python grammar + * * Configure the model to generate valid Python code. See the * [custom grammars guide](https://platform.openai.com/docs/guides/custom-grammars) for more details. * @@ -13150,6 +13320,7 @@ export type ResponseFunctionCallArgumentsDoneEvent = { /** * ResponseImageGenCallCompletedEvent + * * Emitted when an image generation tool call has completed and the final image is available. * */ @@ -13174,6 +13345,7 @@ export type ResponseImageGenCallCompletedEvent = { /** * ResponseImageGenCallGeneratingEvent + * * Emitted when an image generation tool call is actively generating an image (intermediate state). * */ @@ -13198,6 +13370,7 @@ export type ResponseImageGenCallGeneratingEvent = { /** * ResponseImageGenCallInProgressEvent + * * Emitted when an image generation tool call is in progress. * */ @@ -13222,6 +13395,7 @@ export type ResponseImageGenCallInProgressEvent = { /** * ResponseImageGenCallPartialImageEvent + * * Emitted when a partial image is available during image generation streaming. * */ @@ -13353,6 +13527,7 @@ export type ResponseLogProb = { /** * ResponseMCPCallArgumentsDeltaEvent + * * Emitted when there is a delta (partial update) to the arguments of an MCP tool call. * */ @@ -13382,6 +13557,7 @@ export type ResponseMcpCallArgumentsDeltaEvent = { /** * ResponseMCPCallArgumentsDoneEvent + * * Emitted when the arguments for an MCP tool call are finalized. * */ @@ -13411,6 +13587,7 @@ export type ResponseMcpCallArgumentsDoneEvent = { /** * ResponseMCPCallCompletedEvent + * * Emitted when an MCP tool call has completed successfully. * */ @@ -13435,6 +13612,7 @@ export type ResponseMcpCallCompletedEvent = { /** * ResponseMCPCallFailedEvent + * * Emitted when an MCP tool call has failed. * */ @@ -13459,6 +13637,7 @@ export type ResponseMcpCallFailedEvent = { /** * ResponseMCPCallInProgressEvent + * * Emitted when an MCP tool call is in progress. * */ @@ -13483,6 +13662,7 @@ export type ResponseMcpCallInProgressEvent = { /** * ResponseMCPListToolsCompletedEvent + * * Emitted when the list of available MCP tools has been successfully retrieved. * */ @@ -13507,6 +13687,7 @@ export type ResponseMcpListToolsCompletedEvent = { /** * ResponseMCPListToolsFailedEvent + * * Emitted when the attempt to list available MCP tools has failed. * */ @@ -13531,6 +13712,7 @@ export type ResponseMcpListToolsFailedEvent = { /** * ResponseMCPListToolsInProgressEvent + * * Emitted when the system is in the process of retrieving the list of available MCP tools. * */ @@ -13622,6 +13804,7 @@ export type ResponseOutputItemDoneEvent = { /** * ResponseOutputTextAnnotationAddedEvent + * * Emitted when an annotation is added to output text content. * */ @@ -13660,6 +13843,7 @@ export type ResponseOutputTextAnnotationAddedEvent = { /** * Prompt Variables + * * Optional map of values to substitute in for variables in your * prompt. The substitution values can either be strings, or other * Response input types like images or files. @@ -13764,6 +13948,7 @@ export type ResponseProperties = { /** * ResponseQueuedEvent + * * Emitted when a response is queued and waiting to be processed. * */ @@ -14517,7 +14702,6 @@ export type RunGraderRequest = { * The `output_json` variable will be populated if the model sample is a * valid JSON string. * - * */ model_sample: string; }; @@ -14560,6 +14744,7 @@ export type RunGraderResponse = { /** * A run on a thread + * * Represents an execution run on a [thread](https://platform.openai.com/docs/api-reference/threads). */ export type RunObject = { @@ -14702,6 +14887,7 @@ export type RunStepCompletionUsage = { /** * Run step delta object + * * Represents a run step delta i.e. any changed fields on a run step during streaming. * */ @@ -14719,6 +14905,7 @@ export type RunStepDeltaObject = { /** * Message creation + * * Details of the message creation by the run step. */ export type RunStepDeltaStepDetailsMessageCreationObject = { @@ -14736,6 +14923,7 @@ export type RunStepDeltaStepDetailsMessageCreationObject = { /** * Code interpreter tool call + * * Details of the Code Interpreter tool call the run step was involved in. */ export type RunStepDeltaStepDetailsToolCallsCodeObject = { @@ -14795,6 +14983,7 @@ export type RunStepDeltaStepDetailsToolCallsCodeOutputImageObject = { /** * Code interpreter log output + * * Text output from the Code Interpreter tool call as part of a run step. */ export type RunStepDeltaStepDetailsToolCallsCodeOutputLogsObject = { @@ -14873,6 +15062,7 @@ export type RunStepDeltaStepDetailsToolCallsFunctionObject = { /** * Tool calls + * * Details of the tool call. */ export type RunStepDeltaStepDetailsToolCallsObject = { @@ -14889,6 +15079,7 @@ export type RunStepDeltaStepDetailsToolCallsObject = { /** * Message creation + * * Details of the message creation by the run step. */ export type RunStepDetailsMessageCreationObject = { @@ -14906,6 +15097,7 @@ export type RunStepDetailsMessageCreationObject = { /** * Code Interpreter tool call + * * Details of the Code Interpreter tool call the run step was involved in. */ export type RunStepDetailsToolCallsCodeObject = { @@ -14957,6 +15149,7 @@ export type RunStepDetailsToolCallsCodeOutputImageObject = { /** * Code Interpreter log output + * * Text output from the Code Interpreter tool call as part of a run step. */ export type RunStepDetailsToolCallsCodeOutputLogsObject = { @@ -14996,6 +15189,7 @@ export type RunStepDetailsToolCallsFileSearchObject = { /** * File search tool call ranking options + * * The ranking options for the file search. */ export type RunStepDetailsToolCallsFileSearchRankingOptionsObject = { @@ -15008,6 +15202,7 @@ export type RunStepDetailsToolCallsFileSearchRankingOptionsObject = { /** * File search tool call result + * * A result instance of the file search. */ export type RunStepDetailsToolCallsFileSearchResultObject = { @@ -15071,6 +15266,7 @@ export type RunStepDetailsToolCallsFunctionObject = { /** * Tool calls + * * Details of the tool call. */ export type RunStepDetailsToolCallsObject = { @@ -15087,6 +15283,7 @@ export type RunStepDetailsToolCallsObject = { /** * Run steps + * * Represents a step in execution of a run. * */ @@ -15267,6 +15464,7 @@ export type RunToolCallObject = { /** * Screenshot + * * A screenshot action. * */ @@ -15281,6 +15479,7 @@ export type Screenshot = { /** * Scroll + * * A scroll action. * */ @@ -15404,6 +15603,7 @@ export type StaticChunkingStrategy = { /** * Static Chunking Strategy + * * Customize your own chunking strategy by setting chunk size and chunk overlap. */ export type StaticChunkingStrategyRequestParam = { @@ -15484,6 +15684,7 @@ export type TextResponseFormatConfiguration = /** * JSON schema + * * JSON Schema response format. Used to generate structured JSON responses. * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). * @@ -15519,6 +15720,7 @@ export type TextResponseFormatJsonSchema = { /** * Thread + * * Represents a thread that contains [messages](https://platform.openai.com/docs/api-reference/messages). */ export type ThreadObject = { @@ -15608,6 +15810,7 @@ export type Tool = /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -15646,6 +15849,7 @@ export type ToolChoiceAllowed = { /** * Custom tool + * * Use this option to force the model to call a specific custom tool. * */ @@ -15662,6 +15866,7 @@ export type ToolChoiceCustom = { /** * Function tool + * * Use this option to force the model to call a specific function. * */ @@ -15678,6 +15883,7 @@ export type ToolChoiceFunction = { /** * MCP tool + * * Use this option to force the model to call a specific tool on a remote MCP server. * */ @@ -15700,6 +15906,7 @@ export type ToolChoiceMcp = { /** * Tool choice mode + * * Controls which (if any) tool is called by the model. * * `none` means the model will not call any tool and instead generates a message. @@ -15718,6 +15925,7 @@ export const ToolChoiceOptions = { /** * Tool choice mode + * * Controls which (if any) tool is called by the model. * * `none` means the model will not call any tool and instead generates a message. @@ -15733,6 +15941,7 @@ export type ToolChoiceOptions = /** * Hosted tool + * * Indicates that the model should use a built-in tool to generate a response. * [Learn more about built-in tools](https://platform.openai.com/docs/guides/tools). * @@ -15836,6 +16045,7 @@ export type TranscriptTextDoneEvent = { /** * Duration Usage + * * Usage statistics for models billed by audio input duration. */ export type TranscriptTextUsageDuration = { @@ -15851,6 +16061,7 @@ export type TranscriptTextUsageDuration = { /** * Token Usage + * * Usage statistics for models billed by token usage. */ export type TranscriptTextUsageTokens = { @@ -15957,6 +16168,7 @@ export type TranscriptionWord = { /** * Thread Truncation Controls + * * Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run. */ export type TruncationObject = { @@ -15972,6 +16184,7 @@ export type TruncationObject = { /** * Type + * * An action to type in text. * */ @@ -16004,6 +16217,7 @@ export type UpdateVectorStoreRequest = { /** * Upload + * * The Upload object can accept byte chunks in the form of Parts. * */ @@ -16056,6 +16270,7 @@ export type UploadCertificateRequest = { /** * UploadPart + * * The upload Part represents a chunk of bytes we can add to an Upload object. * */ @@ -16474,6 +16689,7 @@ export type ValidateGraderResponse = { /** * Vector store expiration policy + * * The expiration policy for a vector store. */ export type VectorStoreExpirationAfter = { @@ -16501,6 +16717,7 @@ export type VectorStoreFileAttributes = { /** * Vector store file batch + * * A batch of files attached to a vector store. */ export type VectorStoreFileBatchObject = { @@ -16581,6 +16798,7 @@ export type VectorStoreFileContentResponse = { /** * Vector store files + * * A list of files attached to a vector store. */ export type VectorStoreFileObject = { @@ -16627,6 +16845,7 @@ export type VectorStoreFileObject = { /** * Vector store + * * A vector store is a collection of processed files can be used by the `file_search` tool. */ export type VectorStoreObject = { @@ -16790,19 +17009,18 @@ export type Verbosity = (typeof Verbosity)[keyof typeof Verbosity]; export type VoiceIdsShared = | string - | ( - | 'alloy' - | 'ash' - | 'ballad' - | 'coral' - | 'echo' - | 'sage' - | 'shimmer' - | 'verse' - ); + | 'alloy' + | 'ash' + | 'ballad' + | 'coral' + | 'echo' + | 'sage' + | 'shimmer' + | 'verse'; /** * Wait + * * A wait action. * */ @@ -16817,6 +17035,7 @@ export type Wait = { /** * Find action + * * Action type "find": Searches for a pattern within a loaded page. * */ @@ -16840,6 +17059,7 @@ export type WebSearchActionFind = { /** * Open page action + * * Action type "open_page" - Opens a specific URL from search results. * */ @@ -16858,6 +17078,7 @@ export type WebSearchActionOpenPage = { /** * Search action + * * Action type "search" - Performs a web search query. * */ @@ -16895,6 +17116,7 @@ export type WebSearchContextSize = /** * Web search location + * * Approximate location parameters for the search. */ export type WebSearchLocation = { @@ -16925,6 +17147,7 @@ export type WebSearchLocation = { /** * Web search tool call + * * The results of a web search tool call. See the * [web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information. * @@ -16964,6 +17187,7 @@ export type WebSearchToolCall = { /** * batch.cancelled + * * Sent when a batch API request has been cancelled. * */ @@ -17003,6 +17227,7 @@ export type WebhookBatchCancelled = { /** * batch.completed + * * Sent when a batch API request has been completed. * */ @@ -17042,6 +17267,7 @@ export type WebhookBatchCompleted = { /** * batch.expired + * * Sent when a batch API request has expired. * */ @@ -17081,6 +17307,7 @@ export type WebhookBatchExpired = { /** * batch.failed + * * Sent when a batch API request has failed. * */ @@ -17120,6 +17347,7 @@ export type WebhookBatchFailed = { /** * eval.run.canceled + * * Sent when an eval run has been canceled. * */ @@ -17159,6 +17387,7 @@ export type WebhookEvalRunCanceled = { /** * eval.run.failed + * * Sent when an eval run has failed. * */ @@ -17198,6 +17427,7 @@ export type WebhookEvalRunFailed = { /** * eval.run.succeeded + * * Sent when an eval run has succeeded. * */ @@ -17237,6 +17467,7 @@ export type WebhookEvalRunSucceeded = { /** * fine_tuning.job.cancelled + * * Sent when a fine-tuning job has been cancelled. * */ @@ -17276,6 +17507,7 @@ export type WebhookFineTuningJobCancelled = { /** * fine_tuning.job.failed + * * Sent when a fine-tuning job has failed. * */ @@ -17315,6 +17547,7 @@ export type WebhookFineTuningJobFailed = { /** * fine_tuning.job.succeeded + * * Sent when a fine-tuning job has succeeded. * */ @@ -17354,6 +17587,7 @@ export type WebhookFineTuningJobSucceeded = { /** * response.cancelled + * * Sent when a background response has been cancelled. * */ @@ -17393,6 +17627,7 @@ export type WebhookResponseCancelled = { /** * response.completed + * * Sent when a background response has been completed. * */ @@ -17432,6 +17667,7 @@ export type WebhookResponseCompleted = { /** * response.failed + * * Sent when a background response has failed. * */ @@ -17471,6 +17707,7 @@ export type WebhookResponseFailed = { /** * response.incomplete + * * Sent when a background response has been interrupted. * */ @@ -17510,6 +17747,7 @@ export type WebhookResponseIncomplete = { /** * Input text + * * A text input to the model. */ export type InputTextContent = { @@ -17525,6 +17763,7 @@ export type InputTextContent = { /** * Input image + * * An image input to the model. Learn about [image inputs](https://platform.openai.com/docs/guides/vision). */ export type InputImageContent = { @@ -17542,6 +17781,7 @@ export type InputImageContent = { /** * Input file + * * A file input to the model. */ export type InputFileContent = { @@ -17567,6 +17807,7 @@ export type InputFileContent = { /** * Function + * * Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). */ export type FunctionTool = { @@ -17600,6 +17841,7 @@ export type Filters = ComparisonFilter | CompoundFilter; /** * File search + * * A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). */ export type FileSearchTool = { @@ -17635,6 +17877,7 @@ export type ApproximateLocation = { /** * Web search preview + * * This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). */ export type WebSearchPreviewTool = { @@ -17651,6 +17894,7 @@ export type WebSearchPreviewTool = { /** * Computer use preview + * * A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). */ export type ComputerUsePreviewTool = { @@ -17674,6 +17918,7 @@ export type ComputerUsePreviewTool = { /** * Input usage details + * * The input tokens detailed information for the image generation. */ export type ImageGenInputUsageDetails = { @@ -17689,6 +17934,7 @@ export type ImageGenInputUsageDetails = { /** * Image generation usage + * * For `gpt-image-1` only, the token usage information for the image generation. */ export type ImageGenUsage = { @@ -17709,6 +17955,7 @@ export type ImageGenUsage = { /** * File citation + * * A citation to a file. */ export type FileCitationBody = { @@ -17732,6 +17979,7 @@ export type FileCitationBody = { /** * URL citation + * * A citation for a web resource used to generate a model response. */ export type UrlCitationBody = { @@ -17759,6 +18007,7 @@ export type UrlCitationBody = { /** * Container file citation + * * A citation for a container file used to generate a model response. */ export type ContainerFileCitationBody = { @@ -17804,6 +18053,7 @@ export type Annotation = /** * Top log probability + * * The top log probability of a token. */ export type TopLogProb = { @@ -17814,6 +18064,7 @@ export type TopLogProb = { /** * Log probability + * * The log probability of a token. */ export type LogProb = { @@ -17825,6 +18076,7 @@ export type LogProb = { /** * Output text + * * A text output from the model. */ export type OutputTextContent = { @@ -17845,6 +18097,7 @@ export type OutputTextContent = { /** * Refusal + * * A refusal from the model. */ export type RefusalContent = { @@ -17872,6 +18125,7 @@ export type ComputerCallSafetyCheckParam = { /** * Computer tool call output + * * The output of a computer tool call. */ export type ComputerCallOutputItemParam = { @@ -17882,7 +18136,7 @@ export type ComputerCallOutputItemParam = { call_id: string; id?: string | null; output: ComputerScreenshotImage; - status?: ('in_progress' | 'completed' | 'incomplete') | null; + status?: 'in_progress' | 'completed' | 'incomplete' | null; /** * The type of the computer tool call output. Always `computer_call_output`. */ @@ -17891,6 +18145,7 @@ export type ComputerCallOutputItemParam = { /** * Function tool call output + * * The output of a function tool call. */ export type FunctionCallOutputItemParam = { @@ -17903,7 +18158,7 @@ export type FunctionCallOutputItemParam = { * A JSON string of the output of the function tool call. */ output: string; - status?: ('in_progress' | 'completed' | 'incomplete') | null; + status?: 'in_progress' | 'completed' | 'incomplete' | null; /** * The type of the function tool call output. Always `function_call_output`. */ @@ -17912,6 +18167,7 @@ export type FunctionCallOutputItemParam = { /** * Item reference + * * An internal identifier for an item to reference. */ export type ItemReferenceParam = { @@ -18135,43 +18391,62 @@ export type MessageContentDelta = } & MessageDeltaContentImageUrlObject); export const ChatModel = { - CHATGPT_4O_LATEST: 'chatgpt-4o-latest', - CODEX_MINI_LATEST: 'codex-mini-latest', - GPT_3_5_TURBO: 'gpt-3.5-turbo', - GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', - GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', - GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', - GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', - GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', - GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', - GPT_4: 'gpt-4', GPT_4O: 'gpt-4o', GPT_4O_2024_05_13: 'gpt-4o-2024-05-13', GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', GPT_4O_2024_11_20: 'gpt-4o-2024-11-20', GPT_4O_AUDIO_PREVIEW: 'gpt-4o-audio-preview', GPT_4O_AUDIO_PREVIEW_2024_10_01: 'gpt-4o-audio-preview-2024-10-01', + GPT_4_1: 'gpt-4.1', GPT_4O_AUDIO_PREVIEW_2024_12_17: 'gpt-4o-audio-preview-2024-12-17', + GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', GPT_4O_AUDIO_PREVIEW_2025_06_03: 'gpt-4o-audio-preview-2025-06-03', + GPT_4_1_MINI: 'gpt-4.1-mini', + CHATGPT_4O_LATEST: 'chatgpt-4o-latest', + GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', + CODEX_MINI_LATEST: 'codex-mini-latest', + GPT_4_1_NANO: 'gpt-4.1-nano', GPT_4O_MINI: 'gpt-4o-mini', + GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', GPT_4O_MINI_2024_07_18: 'gpt-4o-mini-2024-07-18', + GPT_5: 'gpt-5', + GPT_4: 'gpt-4', + GPT_5_2025_08_07: 'gpt-5-2025-08-07', GPT_4O_MINI_AUDIO_PREVIEW: 'gpt-4o-mini-audio-preview', + GPT_5_CHAT_LATEST: 'gpt-5-chat-latest', GPT_4O_MINI_AUDIO_PREVIEW_2024_12_17: 'gpt-4o-mini-audio-preview-2024-12-17', + GPT_5_MINI: 'gpt-5-mini', + GPT_3_5_TURBO: 'gpt-3.5-turbo', + GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', + GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', + GPT_5_NANO: 'gpt-5-nano', + GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', + GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', + GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', + O1: 'o1', + GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', + O1_2024_12_17: 'o1-2024-12-17', + GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', + O1_MINI: 'o1-mini', + GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', + O3: 'o3', GPT_4O_MINI_SEARCH_PREVIEW: 'gpt-4o-mini-search-preview', + O3_2025_04_16: 'o3-2025-04-16', GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11: 'gpt-4o-mini-search-preview-2025-03-11', + O4_MINI: 'o4-mini', GPT_4O_SEARCH_PREVIEW: 'gpt-4o-search-preview', + O4_MINI_2025_04_16: 'o4-mini-2025-04-16', GPT_4O_SEARCH_PREVIEW_2025_03_11: 'gpt-4o-search-preview-2025-03-11', + O3_MINI: 'o3-mini', GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', + O3_MINI_2025_01_31: 'o3-mini-2025-01-31', GPT_4_0314: 'gpt-4-0314', + O1_PREVIEW: 'o1-preview', GPT_4_0613: 'gpt-4-0613', - GPT_4_1: 'gpt-4.1', + O1_PREVIEW_2024_09_12: 'o1-preview-2024-09-12', GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', - GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', - GPT_4_1_MINI: 'gpt-4.1-mini', - GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', - GPT_4_1_NANO: 'gpt-4.1-nano', - GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', + O1_MINI_2024_09_12: 'o1-mini-2024-09-12', GPT_4_32K: 'gpt-4-32k', GPT_4_32K_0314: 'gpt-4-32k-0314', GPT_4_32K_0613: 'gpt-4-32k-0613', @@ -18179,25 +18454,6 @@ export const ChatModel = { GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', - GPT_5: 'gpt-5', - GPT_5_2025_08_07: 'gpt-5-2025-08-07', - GPT_5_CHAT_LATEST: 'gpt-5-chat-latest', - GPT_5_MINI: 'gpt-5-mini', - GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', - GPT_5_NANO: 'gpt-5-nano', - GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', - O1: 'o1', - O1_2024_12_17: 'o1-2024-12-17', - O1_MINI: 'o1-mini', - O1_MINI_2024_09_12: 'o1-mini-2024-09-12', - O1_PREVIEW: 'o1-preview', - O1_PREVIEW_2024_09_12: 'o1-preview-2024-09-12', - O3: 'o3', - O3_2025_04_16: 'o3-2025-04-16', - O3_MINI: 'o3-mini', - O3_MINI_2025_01_31: 'o3-mini-2025-01-31', - O4_MINI: 'o4-mini', - O4_MINI_2025_04_16: 'o4-mini-2025-04-16', } as const; export type ChatModel = (typeof ChatModel)[keyof typeof ChatModel]; @@ -18227,46 +18483,44 @@ export type CreateThreadAndRunRequestWithoutStream = { */ model?: | string - | ( - | 'gpt-5' - | 'gpt-5-mini' - | 'gpt-5-nano' - | 'gpt-5-2025-08-07' - | 'gpt-5-mini-2025-08-07' - | 'gpt-5-nano-2025-08-07' - | 'gpt-4.1' - | 'gpt-4.1-mini' - | 'gpt-4.1-nano' - | 'gpt-4.1-2025-04-14' - | 'gpt-4.1-mini-2025-04-14' - | 'gpt-4.1-nano-2025-04-14' - | 'gpt-4o' - | 'gpt-4o-2024-11-20' - | 'gpt-4o-2024-08-06' - | 'gpt-4o-2024-05-13' - | 'gpt-4o-mini' - | 'gpt-4o-mini-2024-07-18' - | 'gpt-4.5-preview' - | 'gpt-4.5-preview-2025-02-27' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - ); + | 'gpt-5' + | 'gpt-5-mini' + | 'gpt-5-nano' + | 'gpt-5-2025-08-07' + | 'gpt-5-mini-2025-08-07' + | 'gpt-5-nano-2025-08-07' + | 'gpt-4.1' + | 'gpt-4.1-mini' + | 'gpt-4.1-nano' + | 'gpt-4.1-2025-04-14' + | 'gpt-4.1-mini-2025-04-14' + | 'gpt-4.1-nano-2025-04-14' + | 'gpt-4o' + | 'gpt-4o-2024-11-20' + | 'gpt-4o-2024-08-06' + | 'gpt-4o-2024-05-13' + | 'gpt-4o-mini' + | 'gpt-4o-mini-2024-07-18' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' + | 'gpt-4-turbo' + | 'gpt-4-turbo-2024-04-09' + | 'gpt-4-0125-preview' + | 'gpt-4-turbo-preview' + | 'gpt-4-1106-preview' + | 'gpt-4-vision-preview' + | 'gpt-4' + | 'gpt-4-0314' + | 'gpt-4-0613' + | 'gpt-4-32k' + | 'gpt-4-32k-0314' + | 'gpt-4-32k-0613' + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-0613' + | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-0125' + | 'gpt-3.5-turbo-16k-0613'; parallel_tool_calls?: ParallelToolCalls; response_format?: AssistantsApiResponseFormatOption; /** @@ -23055,6 +23309,172 @@ export type SearchVectorStoreResponses = { export type SearchVectorStoreResponse = SearchVectorStoreResponses[keyof SearchVectorStoreResponses]; -export type ClientOptions = { - baseUrl: 'https://api.openai.com/v1' | (string & {}); +/** + * The event payload sent by the API. + */ +export type PostBatchCancelledWebhookPayload = WebhookBatchCancelled; + +export type PostBatchCancelledWebhookRequest = { + body: PostBatchCancelledWebhookPayload; + key: 'batch_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchCompletedWebhookPayload = WebhookBatchCompleted; + +export type PostBatchCompletedWebhookRequest = { + body: PostBatchCompletedWebhookPayload; + key: 'batch_completed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchExpiredWebhookPayload = WebhookBatchExpired; + +export type PostBatchExpiredWebhookRequest = { + body: PostBatchExpiredWebhookPayload; + key: 'batch_expired'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchFailedWebhookPayload = WebhookBatchFailed; + +export type PostBatchFailedWebhookRequest = { + body: PostBatchFailedWebhookPayload; + key: 'batch_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunCanceledWebhookPayload = WebhookEvalRunCanceled; + +export type PostEvalRunCanceledWebhookRequest = { + body: PostEvalRunCanceledWebhookPayload; + key: 'eval_run_canceled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunFailedWebhookPayload = WebhookEvalRunFailed; + +export type PostEvalRunFailedWebhookRequest = { + body: PostEvalRunFailedWebhookPayload; + key: 'eval_run_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunSucceededWebhookPayload = WebhookEvalRunSucceeded; + +export type PostEvalRunSucceededWebhookRequest = { + body: PostEvalRunSucceededWebhookPayload; + key: 'eval_run_succeeded'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobCancelledWebhookPayload = + WebhookFineTuningJobCancelled; + +export type PostFineTuningJobCancelledWebhookRequest = { + body: PostFineTuningJobCancelledWebhookPayload; + key: 'fine_tuning_job_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobFailedWebhookPayload = WebhookFineTuningJobFailed; + +export type PostFineTuningJobFailedWebhookRequest = { + body: PostFineTuningJobFailedWebhookPayload; + key: 'fine_tuning_job_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobSucceededWebhookPayload = + WebhookFineTuningJobSucceeded; + +export type PostFineTuningJobSucceededWebhookRequest = { + body: PostFineTuningJobSucceededWebhookPayload; + key: 'fine_tuning_job_succeeded'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseCancelledWebhookPayload = WebhookResponseCancelled; + +export type PostResponseCancelledWebhookRequest = { + body: PostResponseCancelledWebhookPayload; + key: 'response_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseCompletedWebhookPayload = WebhookResponseCompleted; + +export type PostResponseCompletedWebhookRequest = { + body: PostResponseCompletedWebhookPayload; + key: 'response_completed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseFailedWebhookPayload = WebhookResponseFailed; + +export type PostResponseFailedWebhookRequest = { + body: PostResponseFailedWebhookPayload; + key: 'response_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseIncompleteWebhookPayload = WebhookResponseIncomplete; + +export type PostResponseIncompleteWebhookRequest = { + body: PostResponseIncompleteWebhookPayload; + key: 'response_incomplete'; + path?: never; + query?: never; }; diff --git a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts index 1564abf60..921fe1fc4 100644 --- a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts +++ b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts @@ -2,8 +2,8 @@ import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@pinia/colada' +import { serializeQueryKeyValue } from '../client' import { client } from '../client.gen' -import { serializeQueryKeyValue } from '../core/queryKeySerializer.gen' import { addPet, createUser, @@ -57,6 +57,7 @@ import type { /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -74,6 +75,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -90,7 +92,7 @@ export const updatePetMutation = ( }) export type QueryKey = [ - Pick & { + Pick & { _id: string baseUrl?: _JSONValue body?: _JSONValue @@ -131,6 +133,7 @@ const createQueryKey = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusQuery = defineQueryOptions( @@ -149,6 +152,7 @@ export const findPetsByStatusQuery = defineQueryOptions( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTagsQuery = defineQueryOptions((options: Options) => ({ @@ -165,6 +169,7 @@ export const findPetsByTagsQuery = defineQueryOptions((options: Options) => ({ @@ -198,6 +204,7 @@ export const getPetByIdQuery = defineQueryOptions((options: Options) => ({ @@ -248,6 +257,7 @@ export const getInventoryQuery = defineQueryOptions((options?: Options 10. Other values will generate exceptions. */ export const getOrderByIdQuery = defineQueryOptions((options: Options) => ({ @@ -298,6 +310,7 @@ export const getOrderByIdQuery = defineQueryOptions((options: Options) => ({ @@ -352,6 +367,7 @@ export const loginUserQuery = defineQueryOptions((options?: Options) => ({ @@ -368,6 +384,7 @@ export const logoutUserQuery = defineQueryOptions((options?: Options) => ({ @@ -401,6 +419,7 @@ export const getUserByNameQuery = defineQueryOptions((options: Options( @@ -103,6 +104,7 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( @@ -125,6 +127,7 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( @@ -143,6 +146,7 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( @@ -161,6 +165,7 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( @@ -179,6 +184,7 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( @@ -201,6 +207,7 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( @@ -223,6 +230,7 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( @@ -246,6 +254,7 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( @@ -264,6 +273,7 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( @@ -280,6 +290,7 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( @@ -292,6 +303,7 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( @@ -304,6 +316,7 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( @@ -320,6 +333,7 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( @@ -340,6 +354,7 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( @@ -352,6 +367,7 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( @@ -364,6 +380,7 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( @@ -376,6 +393,7 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( @@ -388,6 +406,7 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( diff --git a/examples/openapi-ts-sample/src/client/client.gen.ts b/examples/openapi-ts-sample/src/client/client.gen.ts index 25aa4ccc9..4e785697c 100644 --- a/examples/openapi-ts-sample/src/client/client.gen.ts +++ b/examples/openapi-ts-sample/src/client/client.gen.ts @@ -1,13 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts -import { createClientConfig } from '../hey-api'; import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import { createClientConfig } from './src/hey-api.ts'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -17,14 +17,13 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( createClientConfig( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ), diff --git a/examples/openapi-ts-sample/src/client/client/client.gen.ts b/examples/openapi-ts-sample/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-sample/src/client/client/index.ts b/examples/openapi-ts-sample/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-sample/src/client/client/index.ts +++ b/examples/openapi-ts-sample/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/types.ts b/examples/openapi-ts-sample/src/client/client/types.gen.ts similarity index 69% rename from examples/openapi-ts-tanstack-react-query/src/client/client/types.ts rename to examples/openapi-ts-sample/src/client/client/types.gen.ts index 75a2ffbbe..1a005b51e 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/types.ts +++ b/examples/openapi-ts-sample/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +42,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -128,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -152,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -181,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -195,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts b/examples/openapi-ts-sample/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts rename to examples/openapi-ts-sample/src/client/client/utils.gen.ts index bf3f28250..96de282a8 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts +++ b/examples/openapi-ts-sample/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -182,6 +106,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -231,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -271,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -322,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-sample/src/client/core/auth.ts b/examples/openapi-ts-sample/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-sample/src/client/core/auth.ts rename to examples/openapi-ts-sample/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-sample/src/client/core/auth.ts +++ b/examples/openapi-ts-sample/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-next/src/client/core/bodySerializer.ts b/examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts similarity index 82% rename from examples/openapi-ts-next/src/client/core/bodySerializer.ts rename to examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts index fab971b66..49cd8925e 100644 --- a/examples/openapi-ts-next/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,8 +64,8 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), }; @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-next/src/client/core/params.ts b/examples/openapi-ts-sample/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-next/src/client/core/params.ts rename to examples/openapi-ts-sample/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-next/src/client/core/params.ts +++ b/examples/openapi-ts-sample/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-fastify/src/client/core/pathSerializer.ts b/examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-fastify/src/client/core/pathSerializer.ts rename to examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-fastify/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-next/src/client/core/types.ts b/examples/openapi-ts-sample/src/client/core/types.gen.ts similarity index 68% rename from examples/openapi-ts-next/src/client/core/types.ts rename to examples/openapi-ts-sample/src/client/core/types.gen.ts index 1f8688099..643c070c9 100644 --- a/examples/openapi-ts-next/src/client/core/types.ts +++ b/examples/openapi-ts-sample/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -84,6 +84,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -96,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-sample/src/client/core/utils.gen.ts b/examples/openapi-ts-sample/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-sample/src/client/index.ts b/examples/openapi-ts-sample/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-sample/src/client/index.ts +++ b/examples/openapi-ts-sample/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-sample/src/client/sdk.gen.ts b/examples/openapi-ts-sample/src/client/sdk.gen.ts index d4d59335e..867b5a377 100644 --- a/examples/openapi-ts-sample/src/client/sdk.gen.ts +++ b/examples/openapi-ts-sample/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -100,7 +100,7 @@ import { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -116,16 +116,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ requestValidator: async (data) => await zAddPetData.parseAsync(data), responseValidator: async (data) => await zAddPetResponse.parseAsync(data), security: [ @@ -144,12 +141,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -173,12 +171,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -199,12 +198,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -225,12 +225,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -248,12 +249,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -277,12 +279,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -303,12 +306,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -333,12 +337,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -358,12 +363,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -381,12 +387,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -398,12 +405,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -417,12 +425,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -440,12 +449,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -464,12 +474,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -483,12 +494,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -500,12 +512,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -517,12 +530,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -536,12 +550,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-sample/src/client/types.gen.ts b/examples/openapi-ts-sample/src/client/types.gen.ts index 6d8a6e7b9..a2e6be0fa 100644 --- a/examples/openapi-ts-sample/src/client/types.gen.ts +++ b/examples/openapi-ts-sample/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-sample/src/client/zod.gen.ts b/examples/openapi-ts-sample/src/client/zod.gen.ts index 2cd4b3607..a4730a50d 100644 --- a/examples/openapi-ts-sample/src/client/zod.gen.ts +++ b/examples/openapi-ts-sample/src/client/zod.gen.ts @@ -79,11 +79,9 @@ export const zUpdatePetResponse = zPet; export const zFindPetsByStatusData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), - query: z.optional( - z.object({ - status: z.optional(z.enum(['available', 'pending', 'sold'])), - }), - ), + query: z.object({ + status: z.enum(['available', 'pending', 'sold']), + }), }); /** @@ -94,11 +92,9 @@ export const zFindPetsByStatusResponse = z.array(zPet); export const zFindPetsByTagsData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), - query: z.optional( - z.object({ - tags: z.optional(z.array(z.string())), - }), - ), + query: z.object({ + tags: z.array(z.string()), + }), }); /** @@ -176,7 +172,7 @@ export const zGetInventoryData = z.object({ /** * successful operation */ -export const zGetInventoryResponse = z.object({}); +export const zGetInventoryResponse = z.record(z.string(), z.int()); export const zPlaceOrderData = z.object({ body: z.optional(zOrder), diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts index 890b38180..4952a1ddf 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts @@ -6,7 +6,7 @@ import { queryOptions, } from '@tanstack/angular-query-experimental'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,70 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - tags?: ReadonlyArray; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, - tags?: ReadonlyArray, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: - options?.baseUrl || - (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (tags) { - params.tags = tags; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -132,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -146,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -156,10 +96,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -168,12 +108,53 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( @@ -197,6 +178,7 @@ export const findPetsByTagsQueryKey = (options: Options) => /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTagsOptions = (options: Options) => @@ -215,6 +197,7 @@ export const findPetsByTagsOptions = (options: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -225,10 +208,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -242,6 +225,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -258,32 +242,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -298,10 +259,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -310,29 +271,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -347,10 +288,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -364,6 +305,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -380,29 +322,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -417,10 +339,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -431,6 +353,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -441,10 +364,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -458,6 +381,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -474,29 +398,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -511,10 +415,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -523,32 +427,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -563,10 +444,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -580,6 +461,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -601,6 +483,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -619,6 +502,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -629,10 +513,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -646,6 +530,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -664,6 +549,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -674,10 +560,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts index 3fd4a2427..1555ca230 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts @@ -16,7 +16,16 @@ import { import { firstValueFrom } from 'rxjs'; import { filter } from 'rxjs/operators'; -import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, + ResponseStyle, +} from './types.gen'; import { buildUrl, createConfig, @@ -50,13 +59,18 @@ export const createClient = (config: Config = {}): Client => { ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const requestOptions = < + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', + >( + options: RequestOptions, + ) => { const opts = { ..._config, ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -65,69 +79,86 @@ export const createClient = (config: Config = {}): Client => { inject(HttpClient), ); } else { - assertInInjectionContext(request); + assertInInjectionContext(requestOptions); opts.httpClient = inject(HttpClient); } } - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } - const url = buildUrl(opts); + const url = buildUrl(opts as any); - let req = new HttpRequest( - opts.method, + const req = new HttpRequest( + opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, }, ); - for (const fn of interceptors.request._fns) { + return { opts, req, url }; + }; + + const beforeRequest = async (options: RequestOptions) => { + const { opts, req, url } = requestOptions(options); + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + return { opts, req, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, req: initialReq } = await beforeRequest(options); + + let req = initialReq; + + for (const fn of interceptors.request.fns) { if (fn) { - req = await fn(req, opts); + req = await fn(req, opts as any); } } - let response; - const result = { + const result: { + request: HttpRequest; + response: any; + } = { request: req, - response, + response: null, }; try { - response = await firstValueFrom( - opts.httpClient - .request(req) + result.response = (await firstValueFrom( + opts + .httpClient!.request(req) .pipe(filter((event) => event.type === HttpEventType.Response)), - ); + )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { - response = await fn(response, req, opts); + result.response = await fn(result.response, req, opts as any); } } - let bodyResponse: any = response.body; + let bodyResponse = result.response.body; if (opts.responseValidator) { await opts.responseValidator(bodyResponse); @@ -142,18 +173,18 @@ export const createClient = (config: Config = {}): Client => { : { data: bodyResponse, ...result }; } catch (error) { if (error instanceof HttpErrorResponse) { - response = error; + result.response = error; } let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, - response as HttpResponse, + result.response as any, req, - opts, + opts as any, )) as string; } } @@ -171,20 +202,60 @@ export const createClient = (config: Config = {}): Client => { } }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, + requestOptions: (options) => { + if (options.security) { + throw new Error('Security is not supported in requestOptions'); + } + + if (options.requestValidator) { + throw new Error( + 'Request validation is not supported in requestOptions', + ); + } + + return requestOptions(options).req; + }, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts index e54d1a64e..6cdf8ad48 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts @@ -10,6 +10,10 @@ import type { import type { Injector } from '@angular/core'; import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -63,13 +67,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -93,7 +106,7 @@ export interface ResolvedRequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> extends RequestOptions { +> extends RequestOptions { serializedBody?: string; } @@ -102,45 +115,41 @@ export type RequestResult< TError = unknown, ThrowOnError extends boolean = boolean, TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: HttpRequest; - response: HttpResponse; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record +> = Promise< + ThrowOnError extends true + ? TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: HttpRequest; + response: HttpResponse; + } + : TResponseStyle extends 'data' + ? + | (TData extends Record ? TData[keyof TData] : TData) + | undefined + : + | { + data: TData extends Record ? TData[keyof TData] - : TData) - | undefined - : - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - request: HttpRequest; - response: HttpResponse; - } - | { - data: undefined; - error: TError[keyof TError]; - request: HttpRequest; - response: HttpErrorResponse & { - error: TError[keyof TError] | null; - }; - } - >; + : TData; + error: undefined; + request: HttpRequest; + response: HttpResponse; + } + | { + data: undefined; + error: TError[keyof TError]; + request: HttpRequest; + response: HttpErrorResponse & { + error: TError[keyof TError] | null; + }; + } +>; export interface ClientOptions { baseUrl?: string; @@ -154,19 +163,38 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; +type RequestOptionsFn = < + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: RequestOptions, +) => HttpRequest; + type BuildUrlFn = < TData extends { body?: unknown; @@ -178,13 +206,20 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { interceptors: Middleware< HttpRequest, HttpResponse, unknown, ResolvedRequestOptions >; + requestOptions: RequestOptionsFn; }; /** @@ -212,9 +247,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -226,18 +262,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts index 132dc7ab3..64a5d8b09 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts @@ -194,14 +194,13 @@ export const getParseAs = ( return; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: HttpHeaders; - }) => { - for (const auth of security) { +export const setAuthParams = async ( + options: Pick, 'security'> & + Pick & { + headers: HttpHeaders; + }, +) => { + for (const auth of options.security) { const token = await getAuthToken(auth, options.auth); if (!token) { @@ -218,11 +217,11 @@ export const setAuthParams = async ({ options.query[name] = token; break; case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); + options.headers = options.headers.append('Cookie', `${name}=${token}`); break; case 'header': default: - options.headers.set(name, token); + options.headers = options.headers.set(name, token); break; } @@ -346,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts index 9c8e3ec6d..49cd8925e 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts @@ -23,6 +23,8 @@ const serializeFormDataPair = ( ): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts index 848c7e94c..f424fe675 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts index d00b4cc14..c461fceff 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts @@ -6,7 +6,7 @@ import { type UseMutationOptions, } from '@tanstack/react-query'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,63 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -125,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -139,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -153,10 +100,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -165,16 +112,57 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( - options?: Options, + options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( - options?: Options, + options: Options, ) => queryOptions({ queryFn: async ({ queryKey, signal }) => { @@ -189,14 +177,15 @@ export const findPetsByStatusOptions = ( queryKey: findPetsByStatusQueryKey(options), }); -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options); /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -212,6 +201,7 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -222,10 +212,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -239,6 +229,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -255,32 +246,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -295,10 +263,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -307,29 +275,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -344,10 +292,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -361,6 +309,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -377,29 +326,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -414,10 +343,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -428,6 +357,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -438,10 +368,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -455,6 +385,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -471,29 +402,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -508,10 +419,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -520,32 +431,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -560,10 +448,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -577,6 +465,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -598,6 +487,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -616,6 +506,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -626,10 +517,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -643,6 +534,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -661,6 +553,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -671,10 +564,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-fastify/src/client/client/types.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts similarity index 69% rename from examples/openapi-ts-fastify/src/client/client/types.ts rename to examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts index 75a2ffbbe..1a005b51e 100644 --- a/examples/openapi-ts-fastify/src/client/client/types.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +42,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -128,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -152,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -181,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -195,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-fastify/src/client/client/utils.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-fastify/src/client/client/utils.ts rename to examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts index bf3f28250..96de282a8 100644 --- a/examples/openapi-ts-fastify/src/client/client/utils.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -182,6 +106,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -231,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -271,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -322,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts deleted file mode 100644 index 451c7f30f..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..49cd8925e --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts deleted file mode 100644 index fab971b66..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -) => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts new file mode 100644 index 000000000..71c88e852 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts deleted file mode 100644 index 7559bbb8c..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts +++ /dev/null @@ -1,141 +0,0 @@ -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - key: string; - map?: string; - } - | { - in: Extract; - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts deleted file mode 100644 index d692cf0a3..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts +++ /dev/null @@ -1,179 +0,0 @@ -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts deleted file mode 100644 index 1f8688099..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Auth, AuthToken } from './auth'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer'; - -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; - getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; - request: RequestFn; - setConfig: (config: Config) => Config; - trace: MethodFn; -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-react-query/src/client/index.ts b/examples/openapi-ts-tanstack-react-query/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts index 80fadb421..e9fe1844e 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts @@ -6,7 +6,7 @@ import { queryOptions, } from '@tanstack/svelte-query'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,63 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -125,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -139,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -149,10 +96,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -161,16 +108,57 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( - options?: Options, + options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( - options?: Options, + options: Options, ) => queryOptions({ queryFn: async ({ queryKey, signal }) => { @@ -185,14 +173,15 @@ export const findPetsByStatusOptions = ( queryKey: findPetsByStatusQueryKey(options), }); -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options); /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -208,6 +197,7 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -218,10 +208,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -235,6 +225,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -251,32 +242,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -291,10 +259,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -303,29 +271,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -340,10 +288,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -357,6 +305,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -373,29 +322,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -410,10 +339,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -424,6 +353,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -434,10 +364,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -451,6 +381,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -467,29 +398,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -504,10 +415,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -516,32 +427,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -556,10 +444,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -573,6 +461,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -594,6 +483,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -612,6 +502,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -622,10 +513,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -639,6 +530,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -657,6 +549,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -667,10 +560,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts new file mode 100644 index 000000000..1a005b51e --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts deleted file mode 100644 index 75a2ffbbe..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: (request: Request) => ReturnType; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: Exclude | 'auto' | 'stream'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: Pick & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - Omit; - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'body' | 'headers' | 'url' - > & - TData - : OmitKeys, 'body' | 'url'> & - TData & - Pick, 'headers'> - : TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'headers' | 'url' - > & - TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts new file mode 100644 index 000000000..96de282a8 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts @@ -0,0 +1,336 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts deleted file mode 100644 index bf3f28250..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const createQuerySerializer = ({ - allowReserved, - array, - object, -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved, - explode: true, - name, - style: 'form', - value, - ...array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - - return; - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header || typeof header !== 'object') { - continue; - } - - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } - - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); - } - } - exists(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - return !!this._fns[index]; - } - - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; - } - } - - update(id: number | Interceptor, fn: Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; - return id; - } else { - return false; - } - } - - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; - } -} - -// `createInterceptors()` response, meant for external use as it does not -// expose internals -export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; -} - -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts deleted file mode 100644 index 451c7f30f..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..49cd8925e --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts deleted file mode 100644 index fab971b66..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -) => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts new file mode 100644 index 000000000..71c88e852 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts deleted file mode 100644 index 7559bbb8c..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts +++ /dev/null @@ -1,141 +0,0 @@ -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - key: string; - map?: string; - } - | { - in: Extract; - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts deleted file mode 100644 index d692cf0a3..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts +++ /dev/null @@ -1,179 +0,0 @@ -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts deleted file mode 100644 index 1f8688099..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Auth, AuthToken } from './auth'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer'; - -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; - getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; - request: RequestFn; - setConfig: (config: Config) => Config; - trace: MethodFn; -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts index e317c64d7..9050f1d24 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts @@ -2,7 +2,7 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/vue-query' -import { client as _heyApiClient } from '../client.gen' +import { client } from '../client.gen' import { addPet, createUser, @@ -54,72 +54,19 @@ import type { UploadFileResponse } from '../types.gen' -export type QueryKey = [ - Pick & { - _id: string - _infinite?: boolean - } -] - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl - } as QueryKey[0] - if (infinite) { - params._infinite = infinite - } - if (options?.body) { - params.body = options.body - } - if (options?.headers) { - params.headers = options.headers - } - if (options?.path) { - params.path = options.path - } - if (options?.query) { - params.query = options.query - } - return [params] -} - -export const addPetQueryKey = (options: Options) => createQueryKey('addPet', options) - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: addPetQueryKey(options) - }) - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -130,6 +77,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -140,10 +88,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -152,14 +100,54 @@ export const updatePetMutation = ( return mutationOptions } -export const findPetsByStatusQueryKey = (options?: Options) => +export type QueryKey = [ + Pick & { + _id: string + _infinite?: boolean + tags?: ReadonlyArray + } +] + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl + } as QueryKey[0] + if (infinite) { + params._infinite = infinite + } + if (tags) { + params.tags = tags + } + if (options?.body) { + params.body = options.body + } + if (options?.headers) { + params.headers = options.headers + } + if (options?.path) { + params.path = options.path + } + if (options?.query) { + params.query = options.query + } + return [params] +} + +export const findPetsByStatusQueryKey = (options: Options) => createQueryKey('findPetsByStatus', options) /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ -export const findPetsByStatusOptions = (options?: Options) => +export const findPetsByStatusOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByStatus({ @@ -173,14 +161,15 @@ export const findPetsByStatusOptions = (options?: Options) queryKey: findPetsByStatusQueryKey(options) }) -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options) /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -196,16 +185,17 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -219,6 +209,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -235,29 +226,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options) }) -export const updatePetWithFormQueryKey = (options: Options) => - createQueryKey('updatePetWithForm', options) - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: updatePetWithFormQueryKey(options) - }) - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -268,10 +239,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -280,29 +251,9 @@ export const updatePetWithFormMutation = ( return mutationOptions } -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options) - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: uploadFileQueryKey(options) - }) - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -313,10 +264,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -330,6 +281,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -346,29 +298,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options) }) -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options) - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: placeOrderQueryKey(options) - }) - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -379,10 +311,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -393,16 +325,17 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -416,6 +349,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -432,29 +366,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options) }) -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options) - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: createUserQueryKey(options) - }) - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -465,10 +379,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -477,29 +391,9 @@ export const createUserMutation = ( return mutationOptions } -export const createUsersWithListInputQueryKey = (options?: Options) => - createQueryKey('createUsersWithListInput', options) - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: createUsersWithListInputQueryKey(options) - }) - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -514,10 +408,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -531,6 +425,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -552,6 +447,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -570,16 +466,17 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -593,6 +490,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -611,16 +509,17 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts index 1984dc18c..3431b130d 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts @@ -1,12 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import { - type ClientOptions as DefaultClientOptions, - type Config, - createClient, - createConfig -} from './client' -import type { ClientOptions } from './types.gen' +import { type ClientOptions, type Config, createClient, createConfig } from './client' +import type { ClientOptions as ClientOptions2 } from './types.gen' /** * The `createClientConfig()` function will be called on client initialization @@ -16,12 +11,12 @@ import type { ClientOptions } from './types.gen' * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = ( - override?: Config -) => Config & T> +export type CreateClientConfig = ( + override?: Config +) => Config & T> export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3' }) ) diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..4a4fc4648 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts @@ -0,0 +1,253 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen' +import type { HttpMethod } from '../core/types.gen' +import { getValidRequestBody } from '../core/utils.gen' +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen' +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams +} from './utils.gen' + +type ReqInit = Omit & { + body?: any + headers: ReturnType +} + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config) + + const getConfig = (): Config => ({ ..._config }) + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config) + return getConfig() + } + + const interceptors = createInterceptors() + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined + } + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security + }) + } + + if (opts.requestValidator) { + await opts.requestValidator(opts) + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type') + } + + const url = buildUrl(opts) + + return { opts, url } + } + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options) + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts) + } + + let request = new Request(url, requestInit) + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch! + let response = await _fetch(request) + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts) + } + } + + const result = { + request, + response + } + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json' + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs]() + break + case 'formData': + emptyData = new FormData() + break + case 'stream': + emptyData = response.body + break + case 'json': + default: + emptyData = {} + break + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result + } + } + + let data: any + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs]() + break + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result + } + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data) + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data) + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result + } + } + + const textError = await response.text() + let jsonError: unknown + + try { + jsonError = JSON.parse(textError) + } catch { + // noop + } + + const error = jsonError ?? textError + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string + } + } + + finalError = finalError || ({} as string) + + if (opts.throwOnError) { + throw finalError + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result + } + } + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + url + }) + } + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE') + }, + trace: makeMethodFn('TRACE') + } as Client +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts deleted file mode 100644 index 74e5e6f3a..000000000 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { Client, Config, RequestOptions } from './types' -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams -} from './utils' - -type ReqInit = Omit & { - body?: any - headers: ReturnType -} - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config) - - const getConfig = (): Config => ({ ..._config }) - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config) - return getConfig() - } - - const interceptors = createInterceptors() - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers) - } - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security - }) - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body) - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type') - } - - const url = buildUrl(opts) - const requestInit: ReqInit = { - redirect: 'follow', - ...opts - } - - let request = new Request(url, requestInit) - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts) - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch! - let response = await _fetch(request) - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts) - } - } - - const result = { - request, - response - } - - if (response.ok) { - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result - } - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json' - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result - } - } - - let data = await response[parseAs]() - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data) - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data) - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result - } - } - - let error = await response.text() - - try { - error = JSON.parse(error) - } catch { - // noop - } - - let finalError = error - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string - } - } - - finalError = finalError || ({} as string) - - if (opts.throwOnError) { - throw finalError - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result - } - } - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }) - } -} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts index 6aea820a3..b379bec02 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth' -export type { QuerySerializerOptions } from '../core/bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen' +export type { QuerySerializerOptions } from '../core/bodySerializer.gen' export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer -} from '../core/bodySerializer' -export { buildClientParams } from '../core/params' -export { createClient } from './client' +} from '../core/bodySerializer.gen' +export { buildClientParams } from '../core/params.gen' +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen' +export { createClient } from './client.gen' export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape -} from './types' -export { createConfig, mergeHeaders } from './utils' +} from './types.gen' +export { createConfig, mergeHeaders } from './utils.gen' diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts similarity index 68% rename from examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts index e6e5eea3a..b97f75e3f 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts @@ -1,6 +1,9 @@ -import type { Auth } from '../core/auth' -import type { Client as CoreClient, Config as CoreConfig } from '../core/types' -import type { Middleware } from './utils' +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen' +import type { ServerSentEventsOptions, ServerSentEventsResult } from '../core/serverSentEvents.gen' +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen' +import type { Middleware } from './utils.gen' export type ResponseStyle = 'data' | 'fields' @@ -17,7 +20,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType + fetch?: typeof fetch /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +36,7 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream' + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text' /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +52,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string > extends Config<{ - responseStyle: TResponseStyle - throwOnError: ThrowOnError - }> { + responseStyle: TResponseStyle + throwOnError: ThrowOnError + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +83,14 @@ export interface RequestOptions< url: Url } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string +> extends RequestOptions { + serializedBody?: string +} + export type RequestResult< TData = unknown, TError = unknown, @@ -118,17 +138,26 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields' >( - options: Omit, 'method'> + options: Omit, 'method'> ) => RequestResult +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields' +>( + options: Omit, 'method'> +) => Promise> + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields' >( - options: Omit, 'method'> & - Pick>, 'method'> + options: Omit, 'method'> & + Pick>, 'method'> ) => RequestResult type BuildUrlFn = < @@ -142,8 +171,8 @@ type BuildUrlFn = < options: Pick & Options ) => string -export type Client = CoreClient & { - interceptors: Middleware +export type Client = CoreClient & { + interceptors: Middleware } /** @@ -171,8 +200,12 @@ type OmitKeys = Pick> export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields' -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit export type OptionsLegacyParser< @@ -181,12 +214,13 @@ export type OptionsLegacyParser< TResponseStyle extends ResponseStyle = 'fields' > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys, 'body' | 'headers' | 'url'> & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData + Pick, 'body'> + : OmitKeys, 'url'> & TData diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts similarity index 60% rename from examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts index 5e1bc373c..b42b5d951 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts @@ -1,90 +1,15 @@ -import { getAuthToken } from '../core/auth' -import type { QuerySerializer, QuerySerializerOptions } from '../core/bodySerializer' -import { jsonBodySerializer } from '../core/bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen' +import type { QuerySerializerOptions } from '../core/bodySerializer.gen' +import { jsonBodySerializer } from '../core/bodySerializer.gen' import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam -} from '../core/pathSerializer' -import type { Client, ClientOptions, Config, RequestOptions } from './types' - -interface PathSerializer { - path: Record - url: string -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' -type MatrixStyle = 'label' | 'matrix' | 'simple' -type ArraySeparatorStyle = ArrayStyle | MatrixStyle - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url - const matches = _url.match(PATH_PARAM_RE) - if (matches) { - for (const match of matches) { - let explode = false - let name = match.substring(1, match.length - 1) - let style: ArraySeparatorStyle = 'simple' - - if (name.endsWith('*')) { - explode = true - name = name.substring(0, name.length - 1) - } - - if (name.startsWith('.')) { - name = name.substring(1) - style = 'label' - } else if (name.startsWith(';')) { - name = name.substring(1) - style = 'matrix' - } - - const value = path[name] - - if (value === undefined || value === null) { - continue - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })) - continue - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true - }) - ) - continue - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string - })}` - ) - continue - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string) - ) - url = url.replace(match, replaceValue) - } - } - return url -} +} from '../core/pathSerializer.gen' +import { getUrl } from '../core/utils.gen' +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen' export const createQuerySerializer = ({ allowReserved, @@ -169,6 +94,27 @@ export const getParseAs = (contentType: string | null): Exclude & { + headers: Headers + }, + name?: string +): boolean => { + if (!name) { + return false + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true + } + return false } export const setAuthParams = async ({ @@ -179,6 +125,10 @@ export const setAuthParams = async ({ headers: Headers }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue + } + const token = await getAuthToken(auth, options.auth) if (!token) { @@ -202,13 +152,11 @@ export const setAuthParams = async ({ options.headers.set(name, token) break } - - return } } -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -218,36 +166,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url }) - return url -} - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url -}: { - baseUrl?: string - path?: Record - query?: Record - querySerializer: QuerySerializer - url: string -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}` - let url = (baseUrl ?? '') + pathUrl - if (path) { - url = defaultPathSerializer({ path, url }) - } - let search = query ? querySerializer(query) : '' - if (search.startsWith('?')) { - search = search.substring(1) - } - if (search) { - url += `?${search}` - } - return url -} export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b } @@ -258,16 +176,24 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config } +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers() for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue } - const iterator = header instanceof Headers ? header.entries() : Object.entries(header) + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) for (const [key, value] of iterator) { if (value === null) { @@ -305,61 +231,58 @@ type ResInterceptor = ( ) => Res | Promise class Interceptors { - _fns: (Interceptor | null)[] - - constructor() { - this._fns = [] - } + fns: Array = [] - clear() { - this._fns = [] + clear(): void { + this.fns = [] } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1 - } else { - return this._fns.indexOf(id) + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = null } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id) - return !!this._fns[index] + return Boolean(this.fns[index]) } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = null + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1 } + return this.fns.indexOf(id) } - update(id: number | Interceptor, fn: Interceptor) { + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = fn + if (this.fns[index]) { + this.fns[index] = fn return id - } else { - return false } + return false } - use(fn: Interceptor) { - this._fns = [...this._fns, fn] - return this._fns.length - 1 + use(fn: Interceptor): number { + this.fns.push(fn) + return this.fns.length - 1 } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick>, 'eject' | 'use'> - request: Pick>, 'eject' | 'use'> - response: Pick>, 'eject' | 'use'> + error: Interceptors> + request: Interceptors> + response: Interceptors> } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>() diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts index f3729c26e..dc8ff6197 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined export interface Auth { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts similarity index 79% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts index 1ce742d04..e39c6a559 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts @@ -1,4 +1,6 @@ -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer' +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen' export type QuerySerializer = (query: Record) => string @@ -10,15 +12,17 @@ export interface QuerySerializerOptions { object?: SerializerOptions } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value) + } else if (value instanceof Date) { + data.append(key, value.toISOString()) } else { data.append(key, JSON.stringify(value)) } } -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown) => { +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { if (typeof value === 'string') { data.append(key, value) } else { @@ -27,7 +31,9 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: } export const formDataBodySerializer = { - bodySerializer: | Array>>(body: T) => { + bodySerializer: | Array>>( + body: T + ): FormData => { const data = new FormData() Object.entries(body).forEach(([key, value]) => { @@ -46,12 +52,12 @@ export const formDataBodySerializer = { } export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => (typeof value === 'bigint' ? value.toString() : value)) + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)) } export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T) => { + bodySerializer: | Array>>(body: T): string => { const data = new URLSearchParams() Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts index cfac4fe33..34dddb5fa 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query' export type Field = | { in: Exclude + /** + * Field name. This is the name we want the user to see and use. + */ key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string } | { in: Extract + /** + * Key isn't required for bodies. + */ key?: string map?: string } diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts index e5d30d671..acc13672e 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..7e9d0d1ec --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue } + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined + } + if (typeof value === 'bigint') { + return value.toString() + } + if (value instanceof Date) { + return value.toISOString() + } + return value +} + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer) + if (json === undefined) { + return undefined + } + return JSON.parse(json) as JsonValue + } catch { + return undefined + } +} + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false + } + const prototype = Object.getPrototypeOf(value as object) + return prototype === Object.prototype || prototype === null +} + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) + const result: Record = {} + + for (const [key, value] of entries) { + const existing = result[key] + if (existing === undefined) { + result[key] = value + continue + } + + if (Array.isArray(existing)) { + ;(existing as string[]).push(value) + } else { + result[key] = [existing, value] + } + } + + return result +} + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined + } + + if (typeof value === 'bigint') { + return value.toString() + } + + if (value instanceof Date) { + return value.toISOString() + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value) + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value) + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value) + } + + return undefined +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..372e50cc2 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,241 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen' + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void + serializedBody?: RequestInit['body'] + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise + url: string + } + +export interface StreamEvent { + data: TData + event?: string + id?: string + retry?: number +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + > +} + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000 + let attempt = 0 + const signal = options.signal ?? new AbortController().signal + + while (true) { + if (signal.aborted) break + + attempt++ + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined) + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId) + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) + + if (!response.body) throw new Error('No body in SSE response') + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() + + let buffer = '' + + const abortHandler = () => { + try { + reader.cancel() + } catch { + // noop + } + } + + signal.addEventListener('abort', abortHandler) + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += value + + const chunks = buffer.split('\n\n') + buffer = chunks.pop() ?? '' + + for (const chunk of chunks) { + const lines = chunk.split('\n') + const dataLines: Array = [] + let eventName: string | undefined + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')) + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, '') + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, '') + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10) + if (!Number.isNaN(parsed)) { + retryDelay = parsed + } + } + } + + let data: unknown + let parsedJson = false + + if (dataLines.length) { + const rawData = dataLines.join('\n') + try { + data = JSON.parse(rawData) + parsedJson = true + } catch { + data = rawData + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data) + } + + if (responseTransformer) { + data = await responseTransformer(data) + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay + }) + + if (dataLines.length) { + yield data as any + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler) + reader.releaseLock() + } + + break // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error) + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) + await sleep(backoff) + } + } + } + + const stream = createStream() + + return { stream } +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts similarity index 65% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts index 5914dfccb..647ffcc49 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts @@ -1,24 +1,36 @@ -import type { Auth, AuthToken } from './auth' -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts -export interface Client { +import type { Auth, AuthToken } from './auth.gen' +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen' + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace' + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn - connect: MethodFn - delete: MethodFn - get: MethodFn getConfig: () => Config - head: MethodFn - options: MethodFn - patch: MethodFn - post: MethodFn - put: MethodFn request: RequestFn setConfig: (config: Config) => Config - trace: MethodFn -} +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) export interface Config { /** @@ -48,7 +60,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: 'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE' + method?: Uppercase /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -60,6 +72,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -72,3 +90,15 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..bd078be24 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen' +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam +} from './pathSerializer.gen' + +export interface PathSerializer { + path: Record + url: string +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url + const matches = _url.match(PATH_PARAM_RE) + if (matches) { + for (const match of matches) { + let explode = false + let name = match.substring(1, match.length - 1) + let style: ArraySeparatorStyle = 'simple' + + if (name.endsWith('*')) { + explode = true + name = name.substring(0, name.length - 1) + } + + if (name.startsWith('.')) { + name = name.substring(1) + style = 'label' + } else if (name.startsWith(';')) { + name = name.substring(1) + style = 'matrix' + } + + const value = path[name] + + if (value === undefined || value === null) { + continue + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })) + continue + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true + }) + ) + continue + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string + })}` + ) + continue + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string) + ) + url = url.replace(match, replaceValue) + } + } + return url +} + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url +}: { + baseUrl?: string + path?: Record + query?: Record + querySerializer: QuerySerializer + url: string +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}` + let url = (baseUrl ?? '') + pathUrl + if (path) { + url = defaultPathSerializer({ path, url }) + } + let search = query ? querySerializer(query) : '' + if (search.startsWith('?')) { + search = search.substring(1) + } + if (search) { + url += `?${search}` + } + return url +} + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== '' + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/index.ts b/examples/openapi-ts-tanstack-vue-query/src/client/index.ts index 544a87d45..550b29b77 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen' -export * from './types.gen' +export type * from './types.gen' diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts index f4cf4d370..6743104bb 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client' -import { client as _heyApiClient } from './client.gen' +import type { Client, Options as Options2, TDataShape } from './client' +import { client } from './client.gen' import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,12 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options ) => - (options.client ?? _heyApiClient).post({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -103,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options ) => - (options.client ?? _heyApiClient).put({ + (options.client ?? client).put({ security: [ { scheme: 'bearer', @@ -125,16 +127,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options + options: Options ) => - (options?.client ?? _heyApiClient).get< - FindPetsByStatusResponses, - FindPetsByStatusErrors, - ThrowOnError - >({ + (options.client ?? client).get({ security: [ { scheme: 'bearer', @@ -147,16 +146,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options + options: Options ) => - (options?.client ?? _heyApiClient).get< - FindPetsByTagsResponses, - FindPetsByTagsErrors, - ThrowOnError - >({ + (options.client ?? client).get({ security: [ { scheme: 'bearer', @@ -169,12 +165,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ security: [ { scheme: 'bearer', @@ -187,12 +184,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ security: [ { name: 'api_key', @@ -209,12 +207,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -231,12 +230,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options ) => - (options.client ?? _heyApiClient).post({ + (options.client ?? client).post({ bodySerializer: null, security: [ { @@ -254,12 +254,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ security: [ { name: 'api_key', @@ -272,12 +273,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options ) => - (options?.client ?? _heyApiClient).post({ + (options?.client ?? client).post({ url: '/store/order', ...options, headers: { @@ -288,36 +290,39 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ url: '/store/order/{orderId}', ...options }) /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ url: '/store/order/{orderId}', ...options }) /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).post({ + (options?.client ?? client).post({ url: '/user', ...options, headers: { @@ -328,12 +333,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -348,60 +354,65 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ url: '/user/login', ...options }) /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ url: '/user/logout', ...options }) /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ url: '/user/{username}', ...options }) /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ url: '/user/{username}', ...options }) /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options ) => - (options.client ?? _heyApiClient).put({ + (options.client ?? client).put({ url: '/user/{username}', ...options, headers: { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts index 988a72c69..99ce8e7d8 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}) +} + export type Order = { complete?: boolean id?: number @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses] export type FindPetsByStatusData = { body?: never path?: never - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold' + status: 'available' | 'pending' | 'sold' } url: '/pet/findByStatus' } @@ -168,11 +172,11 @@ export type FindPetsByStatusResponse = FindPetsByStatusResponses[keyof FindPetsB export type FindPetsByTagsData = { body?: never path?: never - query?: { + query: { /** * Tags to filter by */ - tags?: Array + tags: Array } url: '/pet/findByTags' } @@ -555,7 +559,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File + 200: string } export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses] @@ -687,7 +691,3 @@ export type UpdateUserResponses = { */ 200: unknown } - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}) -} diff --git a/package.json b/package.json index ff853ff02..20217c819 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "build": "turbo run build --filter=\"!@example/openapi-ts-sample\"", "changeset": "changeset", "example": "sh ./scripts/example.sh", + "examples:check": "sh ./scripts/examples-check.sh", + "examples:generate": "sh ./scripts/examples-generate.sh", "format": "prettier --write .", "lint:fix": "prettier --check --write . && eslint . --fix", "lint": "prettier --check . && eslint .", diff --git a/scripts/examples-check.sh b/scripts/examples-check.sh new file mode 100755 index 000000000..30cb827e8 --- /dev/null +++ b/scripts/examples-check.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Check if generated client code for all examples is up-to-date +# This script is used in CI to ensure examples are kept in sync with the codebase + +set -e + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "Checking if generated code is up-to-date..." + +# Generate fresh code +"$SCRIPT_DIR/examples-generate.sh" + +# Check if there are any changes +if ! git diff --quiet; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "❌ ERROR: Generated code is out of sync!" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "The following files have changed:" + git diff --name-only + echo "" + echo "To fix this, run:" + echo " pnpm examples:generate" + echo "" + echo "Then commit the changes." + exit 1 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ All generated code is up-to-date!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/scripts/examples-generate.sh b/scripts/examples-generate.sh new file mode 100755 index 000000000..89792b670 --- /dev/null +++ b/scripts/examples-generate.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Generate client code for all examples that have openapi-ts script +# This script is used to ensure examples are up-to-date with the latest code + +set -e + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "Generating client code for all examples..." + +# Find all examples with openapi-ts script and generate code +for dir in "$ROOT_DIR"/examples/*/; do + example_name=$(basename "$dir") + package_json="$dir/package.json" + + # Skip if package.json doesn't exist + if [ ! -f "$package_json" ]; then + continue + fi + + # Check if the example has openapi-ts script + if grep -q "\"openapi-ts\":" "$package_json"; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📦 Generating: $example_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Run openapi-ts for this example + (cd "$dir" && pnpm openapi-ts) + + echo "✅ Completed: $example_name" + fi +done + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✨ All examples generated successfully!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"