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

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3374 from hey-api/copilot/fix-date-transformers-generation

fix(transformers): generate date transformers for allOf response schemas

authored by

Lubos and committed by
GitHub
5627f122 895e121d

+4244 -1
+5
.changeset/shy-falcons-press.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **plugin(@hey-api/transformers)**: fix: handle `$ref` keywords in `allOf` compositions
+8
packages/openapi-ts-tests/main/test/3.0.x.test.ts
··· 671 671 }, 672 672 { 673 673 config: createConfig({ 674 + input: 'transformers-allof-response-wrapper.json', 675 + output: 'transformers-allof-response-wrapper', 676 + plugins: ['@hey-api/client-fetch', '@hey-api/transformers'], 677 + }), 678 + description: 'transforms dates in allOf response wrapper (paginated response)', 679 + }, 680 + { 681 + config: createConfig({ 674 682 input: 'transformers-any-of-null.json', 675 683 output: 'transformers-any-of-null', 676 684 plugins: ['@hey-api/client-fetch', '@hey-api/transformers'],
+8
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 825 825 }, 826 826 { 827 827 config: createConfig({ 828 + input: 'transformers-allof-response-wrapper.json', 829 + output: 'transformers-allof-response-wrapper', 830 + plugins: ['@hey-api/client-fetch', '@hey-api/transformers'], 831 + }), 832 + description: 'transforms dates in allOf response wrapper (paginated response)', 833 + }, 834 + { 835 + config: createConfig({ 828 836 input: 'transformers-any-of-null.json', 829 837 output: 'transformers-any-of-null', 830 838 plugins: ['@hey-api/client-fetch', '@hey-api/transformers'],
+16
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { type ClientOptions, type Config, createClient, createConfig } from './client'; 4 + import type { ClientOptions as ClientOptions2 } from './types.gen'; 5 + 6 + /** 7 + * The `createClientConfig()` function will be called on client initialization 8 + * and the returned object will become the client's initial configuration. 9 + * 10 + * You may want to initialize your client this way instead of calling 11 + * `setConfig()`. This is useful for example if you're using Next.js 12 + * to ensure your client always has the correct values. 13 + */ 14 + export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>; 15 + 16 + export const client = createClient(createConfig<ClientOptions2>());
+288
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/client/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createSseClient } from '../core/serverSentEvents.gen'; 4 + import type { HttpMethod } from '../core/types.gen'; 5 + import { getValidRequestBody } from '../core/utils.gen'; 6 + import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; 7 + import { 8 + buildUrl, 9 + createConfig, 10 + createInterceptors, 11 + getParseAs, 12 + mergeConfigs, 13 + mergeHeaders, 14 + setAuthParams, 15 + } from './utils.gen'; 16 + 17 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 18 + body?: any; 19 + headers: ReturnType<typeof mergeHeaders>; 20 + }; 21 + 22 + export const createClient = (config: Config = {}): Client => { 23 + let _config = mergeConfigs(createConfig(), config); 24 + 25 + const getConfig = (): Config => ({ ..._config }); 26 + 27 + const setConfig = (config: Config): Config => { 28 + _config = mergeConfigs(_config, config); 29 + return getConfig(); 30 + }; 31 + 32 + const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>(); 33 + 34 + const beforeRequest = async (options: RequestOptions) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + serializedBody: undefined, 41 + }; 42 + 43 + if (opts.security) { 44 + await setAuthParams({ 45 + ...opts, 46 + security: opts.security, 47 + }); 48 + } 49 + 50 + if (opts.requestValidator) { 51 + await opts.requestValidator(opts); 52 + } 53 + 54 + if (opts.body !== undefined && opts.bodySerializer) { 55 + opts.serializedBody = opts.bodySerializer(opts.body); 56 + } 57 + 58 + // remove Content-Type header if body is empty to avoid sending invalid requests 59 + if (opts.body === undefined || opts.serializedBody === '') { 60 + opts.headers.delete('Content-Type'); 61 + } 62 + 63 + const url = buildUrl(opts); 64 + 65 + return { opts, url }; 66 + }; 67 + 68 + const request: Client['request'] = async (options) => { 69 + // @ts-expect-error 70 + const { opts, url } = await beforeRequest(options); 71 + const requestInit: ReqInit = { 72 + redirect: 'follow', 73 + ...opts, 74 + body: getValidRequestBody(opts), 75 + }; 76 + 77 + let request = new Request(url, requestInit); 78 + 79 + for (const fn of interceptors.request.fns) { 80 + if (fn) { 81 + request = await fn(request, opts); 82 + } 83 + } 84 + 85 + // fetch must be assigned here, otherwise it would throw the error: 86 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 87 + const _fetch = opts.fetch!; 88 + let response: Response; 89 + 90 + try { 91 + response = await _fetch(request); 92 + } catch (error) { 93 + // Handle fetch exceptions (AbortError, network errors, etc.) 94 + let finalError = error; 95 + 96 + for (const fn of interceptors.error.fns) { 97 + if (fn) { 98 + finalError = (await fn(error, undefined as any, request, opts)) as unknown; 99 + } 100 + } 101 + 102 + finalError = finalError || ({} as unknown); 103 + 104 + if (opts.throwOnError) { 105 + throw finalError; 106 + } 107 + 108 + // Return error response 109 + return opts.responseStyle === 'data' 110 + ? undefined 111 + : { 112 + error: finalError, 113 + request, 114 + response: undefined as any, 115 + }; 116 + } 117 + 118 + for (const fn of interceptors.response.fns) { 119 + if (fn) { 120 + response = await fn(response, request, opts); 121 + } 122 + } 123 + 124 + const result = { 125 + request, 126 + response, 127 + }; 128 + 129 + if (response.ok) { 130 + const parseAs = 131 + (opts.parseAs === 'auto' 132 + ? getParseAs(response.headers.get('Content-Type')) 133 + : opts.parseAs) ?? 'json'; 134 + 135 + if (response.status === 204 || response.headers.get('Content-Length') === '0') { 136 + let emptyData: any; 137 + switch (parseAs) { 138 + case 'arrayBuffer': 139 + case 'blob': 140 + case 'text': 141 + emptyData = await response[parseAs](); 142 + break; 143 + case 'formData': 144 + emptyData = new FormData(); 145 + break; 146 + case 'stream': 147 + emptyData = response.body; 148 + break; 149 + case 'json': 150 + default: 151 + emptyData = {}; 152 + break; 153 + } 154 + return opts.responseStyle === 'data' 155 + ? emptyData 156 + : { 157 + data: emptyData, 158 + ...result, 159 + }; 160 + } 161 + 162 + let data: any; 163 + switch (parseAs) { 164 + case 'arrayBuffer': 165 + case 'blob': 166 + case 'formData': 167 + case 'text': 168 + data = await response[parseAs](); 169 + break; 170 + case 'json': { 171 + // Some servers return 200 with no Content-Length and empty body. 172 + // response.json() would throw; read as text and parse if non-empty. 173 + const text = await response.text(); 174 + data = text ? JSON.parse(text) : {}; 175 + break; 176 + } 177 + case 'stream': 178 + return opts.responseStyle === 'data' 179 + ? response.body 180 + : { 181 + data: response.body, 182 + ...result, 183 + }; 184 + } 185 + 186 + if (parseAs === 'json') { 187 + if (opts.responseValidator) { 188 + await opts.responseValidator(data); 189 + } 190 + 191 + if (opts.responseTransformer) { 192 + data = await opts.responseTransformer(data); 193 + } 194 + } 195 + 196 + return opts.responseStyle === 'data' 197 + ? data 198 + : { 199 + data, 200 + ...result, 201 + }; 202 + } 203 + 204 + const textError = await response.text(); 205 + let jsonError: unknown; 206 + 207 + try { 208 + jsonError = JSON.parse(textError); 209 + } catch { 210 + // noop 211 + } 212 + 213 + const error = jsonError ?? textError; 214 + let finalError = error; 215 + 216 + for (const fn of interceptors.error.fns) { 217 + if (fn) { 218 + finalError = (await fn(error, response, request, opts)) as string; 219 + } 220 + } 221 + 222 + finalError = finalError || ({} as string); 223 + 224 + if (opts.throwOnError) { 225 + throw finalError; 226 + } 227 + 228 + // TODO: we probably want to return error and improve types 229 + return opts.responseStyle === 'data' 230 + ? undefined 231 + : { 232 + error: finalError, 233 + ...result, 234 + }; 235 + }; 236 + 237 + const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 238 + request({ ...options, method }); 239 + 240 + const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 241 + const { opts, url } = await beforeRequest(options); 242 + return createSseClient({ 243 + ...opts, 244 + body: opts.body as BodyInit | null | undefined, 245 + headers: opts.headers as unknown as Record<string, string>, 246 + method, 247 + onRequest: async (url, init) => { 248 + let request = new Request(url, init); 249 + for (const fn of interceptors.request.fns) { 250 + if (fn) { 251 + request = await fn(request, opts); 252 + } 253 + } 254 + return request; 255 + }, 256 + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, 257 + url, 258 + }); 259 + }; 260 + 261 + return { 262 + buildUrl, 263 + connect: makeMethodFn('CONNECT'), 264 + delete: makeMethodFn('DELETE'), 265 + get: makeMethodFn('GET'), 266 + getConfig, 267 + head: makeMethodFn('HEAD'), 268 + interceptors, 269 + options: makeMethodFn('OPTIONS'), 270 + patch: makeMethodFn('PATCH'), 271 + post: makeMethodFn('POST'), 272 + put: makeMethodFn('PUT'), 273 + request, 274 + setConfig, 275 + sse: { 276 + connect: makeSseFn('CONNECT'), 277 + delete: makeSseFn('DELETE'), 278 + get: makeSseFn('GET'), 279 + head: makeSseFn('HEAD'), 280 + options: makeSseFn('OPTIONS'), 281 + patch: makeSseFn('PATCH'), 282 + post: makeSseFn('POST'), 283 + put: makeSseFn('PUT'), 284 + trace: makeSseFn('TRACE'), 285 + }, 286 + trace: makeMethodFn('TRACE'), 287 + } as Client; 288 + };
+25
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/client/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + export { 6 + formDataBodySerializer, 7 + jsonBodySerializer, 8 + urlSearchParamsBodySerializer, 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; 12 + export { createClient } from './client.gen'; 13 + export type { 14 + Client, 15 + ClientOptions, 16 + Config, 17 + CreateClientConfig, 18 + Options, 19 + RequestOptions, 20 + RequestResult, 21 + ResolvedRequestOptions, 22 + ResponseStyle, 23 + TDataShape, 24 + } from './types.gen'; 25 + export { createConfig, mergeHeaders } from './utils.gen';
+213
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/client/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth } from '../core/auth.gen'; 4 + import type { 5 + ServerSentEventsOptions, 6 + ServerSentEventsResult, 7 + } from '../core/serverSentEvents.gen'; 8 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; 9 + import type { Middleware } from './utils.gen'; 10 + 11 + export type ResponseStyle = 'data' | 'fields'; 12 + 13 + export interface Config<T extends ClientOptions = ClientOptions> 14 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig { 15 + /** 16 + * Base URL for all requests made by this client. 17 + */ 18 + baseUrl?: T['baseUrl']; 19 + /** 20 + * Fetch API implementation. You can use this option to provide a custom 21 + * fetch instance. 22 + * 23 + * @default globalThis.fetch 24 + */ 25 + fetch?: typeof fetch; 26 + /** 27 + * Please don't use the Fetch client for Next.js applications. The `next` 28 + * options won't have any effect. 29 + * 30 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 31 + */ 32 + next?: never; 33 + /** 34 + * Return the response data parsed in a specified format. By default, `auto` 35 + * will infer the appropriate method from the `Content-Type` response header. 36 + * You can override this behavior with any of the {@link Body} methods. 37 + * Select `stream` if you don't want to parse response data at all. 38 + * 39 + * @default 'auto' 40 + */ 41 + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; 42 + /** 43 + * Should we return only data or multiple fields (data, error, response, etc.)? 44 + * 45 + * @default 'fields' 46 + */ 47 + responseStyle?: ResponseStyle; 48 + /** 49 + * Throw an error instead of returning it in the response? 50 + * 51 + * @default false 52 + */ 53 + throwOnError?: T['throwOnError']; 54 + } 55 + 56 + export interface RequestOptions< 57 + TData = unknown, 58 + TResponseStyle extends ResponseStyle = 'fields', 59 + ThrowOnError extends boolean = boolean, 60 + Url extends string = string, 61 + > 62 + extends 63 + Config<{ 64 + responseStyle: TResponseStyle; 65 + throwOnError: ThrowOnError; 66 + }>, 67 + Pick< 68 + ServerSentEventsOptions<TData>, 69 + | 'onSseError' 70 + | 'onSseEvent' 71 + | 'sseDefaultRetryDelay' 72 + | 'sseMaxRetryAttempts' 73 + | 'sseMaxRetryDelay' 74 + > { 75 + /** 76 + * Any body that you want to add to your request. 77 + * 78 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 79 + */ 80 + body?: unknown; 81 + path?: Record<string, unknown>; 82 + query?: Record<string, unknown>; 83 + /** 84 + * Security mechanism(s) to use for the request. 85 + */ 86 + security?: ReadonlyArray<Auth>; 87 + url: Url; 88 + } 89 + 90 + export interface ResolvedRequestOptions< 91 + TResponseStyle extends ResponseStyle = 'fields', 92 + ThrowOnError extends boolean = boolean, 93 + Url extends string = string, 94 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 95 + serializedBody?: string; 96 + } 97 + 98 + export type RequestResult< 99 + TData = unknown, 100 + TError = unknown, 101 + ThrowOnError extends boolean = boolean, 102 + TResponseStyle extends ResponseStyle = 'fields', 103 + > = ThrowOnError extends true 104 + ? Promise< 105 + TResponseStyle extends 'data' 106 + ? TData extends Record<string, unknown> 107 + ? TData[keyof TData] 108 + : TData 109 + : { 110 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; 111 + request: Request; 112 + response: Response; 113 + } 114 + > 115 + : Promise< 116 + TResponseStyle extends 'data' 117 + ? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined 118 + : ( 119 + | { 120 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; 121 + error: undefined; 122 + } 123 + | { 124 + data: undefined; 125 + error: TError extends Record<string, unknown> ? TError[keyof TError] : TError; 126 + } 127 + ) & { 128 + request: Request; 129 + response: Response; 130 + } 131 + >; 132 + 133 + export interface ClientOptions { 134 + baseUrl?: string; 135 + responseStyle?: ResponseStyle; 136 + throwOnError?: boolean; 137 + } 138 + 139 + type MethodFn = < 140 + TData = unknown, 141 + TError = unknown, 142 + ThrowOnError extends boolean = false, 143 + TResponseStyle extends ResponseStyle = 'fields', 144 + >( 145 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 146 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 147 + 148 + type SseFn = < 149 + TData = unknown, 150 + TError = unknown, 151 + ThrowOnError extends boolean = false, 152 + TResponseStyle extends ResponseStyle = 'fields', 153 + >( 154 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 155 + ) => Promise<ServerSentEventsResult<TData, TError>>; 156 + 157 + type RequestFn = < 158 + TData = unknown, 159 + TError = unknown, 160 + ThrowOnError extends boolean = false, 161 + TResponseStyle extends ResponseStyle = 'fields', 162 + >( 163 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 164 + Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>, 165 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 166 + 167 + type BuildUrlFn = < 168 + TData extends { 169 + body?: unknown; 170 + path?: Record<string, unknown>; 171 + query?: Record<string, unknown>; 172 + url: string; 173 + }, 174 + >( 175 + options: TData & Options<TData>, 176 + ) => string; 177 + 178 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & { 179 + interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; 180 + }; 181 + 182 + /** 183 + * The `createClientConfig()` function will be called on client initialization 184 + * and the returned object will become the client's initial configuration. 185 + * 186 + * You may want to initialize your client this way instead of calling 187 + * `setConfig()`. This is useful for example if you're using Next.js 188 + * to ensure your client always has the correct values. 189 + */ 190 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 191 + override?: Config<ClientOptions & T>, 192 + ) => Config<Required<ClientOptions> & T>; 193 + 194 + export interface TDataShape { 195 + body?: unknown; 196 + headers?: unknown; 197 + path?: unknown; 198 + query?: unknown; 199 + url: string; 200 + } 201 + 202 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 203 + 204 + export type Options< 205 + TData extends TDataShape = TDataShape, 206 + ThrowOnError extends boolean = boolean, 207 + TResponse = unknown, 208 + TResponseStyle extends ResponseStyle = 'fields', 209 + > = OmitKeys< 210 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 211 + 'body' | 'path' | 'query' | 'url' 212 + > & 213 + ([TData] extends [never] ? unknown : Omit<TData, 'url'>);
+316
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/client/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { getAuthToken } from '../core/auth.gen'; 4 + import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + import { jsonBodySerializer } from '../core/bodySerializer.gen'; 6 + import { 7 + serializeArrayParam, 8 + serializeObjectParam, 9 + serializePrimitiveParam, 10 + } from '../core/pathSerializer.gen'; 11 + import { getUrl } from '../core/utils.gen'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; 13 + 14 + export const createQuerySerializer = <T = unknown>({ 15 + parameters = {}, 16 + ...args 17 + }: QuerySerializerOptions = {}) => { 18 + const querySerializer = (queryParams: T) => { 19 + const search: string[] = []; 20 + if (queryParams && typeof queryParams === 'object') { 21 + for (const name in queryParams) { 22 + const value = queryParams[name]; 23 + 24 + if (value === undefined || value === null) { 25 + continue; 26 + } 27 + 28 + const options = parameters[name] || args; 29 + 30 + if (Array.isArray(value)) { 31 + const serializedArray = serializeArrayParam({ 32 + allowReserved: options.allowReserved, 33 + explode: true, 34 + name, 35 + style: 'form', 36 + value, 37 + ...options.array, 38 + }); 39 + if (serializedArray) search.push(serializedArray); 40 + } else if (typeof value === 'object') { 41 + const serializedObject = serializeObjectParam({ 42 + allowReserved: options.allowReserved, 43 + explode: true, 44 + name, 45 + style: 'deepObject', 46 + value: value as Record<string, unknown>, 47 + ...options.object, 48 + }); 49 + if (serializedObject) search.push(serializedObject); 50 + } else { 51 + const serializedPrimitive = serializePrimitiveParam({ 52 + allowReserved: options.allowReserved, 53 + name, 54 + value: value as string, 55 + }); 56 + if (serializedPrimitive) search.push(serializedPrimitive); 57 + } 58 + } 59 + } 60 + return search.join('&'); 61 + }; 62 + return querySerializer; 63 + }; 64 + 65 + /** 66 + * Infers parseAs value from provided Content-Type header. 67 + */ 68 + export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => { 69 + if (!contentType) { 70 + // If no Content-Type header is provided, the best we can do is return the raw response body, 71 + // which is effectively the same as the 'stream' option. 72 + return 'stream'; 73 + } 74 + 75 + const cleanContent = contentType.split(';')[0]?.trim(); 76 + 77 + if (!cleanContent) { 78 + return; 79 + } 80 + 81 + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { 82 + return 'json'; 83 + } 84 + 85 + if (cleanContent === 'multipart/form-data') { 86 + return 'formData'; 87 + } 88 + 89 + if ( 90 + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) 91 + ) { 92 + return 'blob'; 93 + } 94 + 95 + if (cleanContent.startsWith('text/')) { 96 + return 'text'; 97 + } 98 + 99 + return; 100 + }; 101 + 102 + const checkForExistence = ( 103 + options: Pick<RequestOptions, 'auth' | 'query'> & { 104 + headers: Headers; 105 + }, 106 + name?: string, 107 + ): boolean => { 108 + if (!name) { 109 + return false; 110 + } 111 + if ( 112 + options.headers.has(name) || 113 + options.query?.[name] || 114 + options.headers.get('Cookie')?.includes(`${name}=`) 115 + ) { 116 + return true; 117 + } 118 + return false; 119 + }; 120 + 121 + export const setAuthParams = async ({ 122 + security, 123 + ...options 124 + }: Pick<Required<RequestOptions>, 'security'> & 125 + Pick<RequestOptions, 'auth' | 'query'> & { 126 + headers: Headers; 127 + }) => { 128 + for (const auth of security) { 129 + if (checkForExistence(options, auth.name)) { 130 + continue; 131 + } 132 + 133 + const token = await getAuthToken(auth, options.auth); 134 + 135 + if (!token) { 136 + continue; 137 + } 138 + 139 + const name = auth.name ?? 'Authorization'; 140 + 141 + switch (auth.in) { 142 + case 'query': 143 + if (!options.query) { 144 + options.query = {}; 145 + } 146 + options.query[name] = token; 147 + break; 148 + case 'cookie': 149 + options.headers.append('Cookie', `${name}=${token}`); 150 + break; 151 + case 'header': 152 + default: 153 + options.headers.set(name, token); 154 + break; 155 + } 156 + } 157 + }; 158 + 159 + export const buildUrl: Client['buildUrl'] = (options) => 160 + getUrl({ 161 + baseUrl: options.baseUrl as string, 162 + path: options.path, 163 + query: options.query, 164 + querySerializer: 165 + typeof options.querySerializer === 'function' 166 + ? options.querySerializer 167 + : createQuerySerializer(options.querySerializer), 168 + url: options.url, 169 + }); 170 + 171 + export const mergeConfigs = (a: Config, b: Config): Config => { 172 + const config = { ...a, ...b }; 173 + if (config.baseUrl?.endsWith('/')) { 174 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 175 + } 176 + config.headers = mergeHeaders(a.headers, b.headers); 177 + return config; 178 + }; 179 + 180 + const headersEntries = (headers: Headers): Array<[string, string]> => { 181 + const entries: Array<[string, string]> = []; 182 + headers.forEach((value, key) => { 183 + entries.push([key, value]); 184 + }); 185 + return entries; 186 + }; 187 + 188 + export const mergeHeaders = ( 189 + ...headers: Array<Required<Config>['headers'] | undefined> 190 + ): Headers => { 191 + const mergedHeaders = new Headers(); 192 + for (const header of headers) { 193 + if (!header) { 194 + continue; 195 + } 196 + 197 + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); 198 + 199 + for (const [key, value] of iterator) { 200 + if (value === null) { 201 + mergedHeaders.delete(key); 202 + } else if (Array.isArray(value)) { 203 + for (const v of value) { 204 + mergedHeaders.append(key, v as string); 205 + } 206 + } else if (value !== undefined) { 207 + // assume object headers are meant to be JSON stringified, i.e. their 208 + // content value in OpenAPI specification is 'application/json' 209 + mergedHeaders.set( 210 + key, 211 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 212 + ); 213 + } 214 + } 215 + } 216 + return mergedHeaders; 217 + }; 218 + 219 + type ErrInterceptor<Err, Res, Req, Options> = ( 220 + error: Err, 221 + response: Res, 222 + request: Req, 223 + options: Options, 224 + ) => Err | Promise<Err>; 225 + 226 + type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>; 227 + 228 + type ResInterceptor<Res, Req, Options> = ( 229 + response: Res, 230 + request: Req, 231 + options: Options, 232 + ) => Res | Promise<Res>; 233 + 234 + class Interceptors<Interceptor> { 235 + fns: Array<Interceptor | null> = []; 236 + 237 + clear(): void { 238 + this.fns = []; 239 + } 240 + 241 + eject(id: number | Interceptor): void { 242 + const index = this.getInterceptorIndex(id); 243 + if (this.fns[index]) { 244 + this.fns[index] = null; 245 + } 246 + } 247 + 248 + exists(id: number | Interceptor): boolean { 249 + const index = this.getInterceptorIndex(id); 250 + return Boolean(this.fns[index]); 251 + } 252 + 253 + getInterceptorIndex(id: number | Interceptor): number { 254 + if (typeof id === 'number') { 255 + return this.fns[id] ? id : -1; 256 + } 257 + return this.fns.indexOf(id); 258 + } 259 + 260 + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { 261 + const index = this.getInterceptorIndex(id); 262 + if (this.fns[index]) { 263 + this.fns[index] = fn; 264 + return id; 265 + } 266 + return false; 267 + } 268 + 269 + use(fn: Interceptor): number { 270 + this.fns.push(fn); 271 + return this.fns.length - 1; 272 + } 273 + } 274 + 275 + export interface Middleware<Req, Res, Err, Options> { 276 + error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 277 + request: Interceptors<ReqInterceptor<Req, Options>>; 278 + response: Interceptors<ResInterceptor<Res, Req, Options>>; 279 + } 280 + 281 + export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 282 + Req, 283 + Res, 284 + Err, 285 + Options 286 + > => ({ 287 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 288 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 289 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 290 + }); 291 + 292 + const defaultQuerySerializer = createQuerySerializer({ 293 + allowReserved: false, 294 + array: { 295 + explode: true, 296 + style: 'form', 297 + }, 298 + object: { 299 + explode: true, 300 + style: 'deepObject', 301 + }, 302 + }); 303 + 304 + const defaultHeaders = { 305 + 'Content-Type': 'application/json', 306 + }; 307 + 308 + export const createConfig = <T extends ClientOptions = ClientOptions>( 309 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 310 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 311 + ...jsonBodySerializer, 312 + headers: defaultHeaders, 313 + parseAs: 'auto', 314 + querySerializer: defaultQuerySerializer, 315 + ...override, 316 + });
+41
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type AuthToken = string | undefined; 4 + 5 + export interface Auth { 6 + /** 7 + * Which part of the request do we use to send the auth? 8 + * 9 + * @default 'header' 10 + */ 11 + in?: 'header' | 'query' | 'cookie'; 12 + /** 13 + * Header or query parameter name. 14 + * 15 + * @default 'Authorization' 16 + */ 17 + name?: string; 18 + scheme?: 'basic' | 'bearer'; 19 + type: 'apiKey' | 'http'; 20 + } 21 + 22 + export const getAuthToken = async ( 23 + auth: Auth, 24 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 25 + ): Promise<string | undefined> => { 26 + const token = typeof callback === 'function' ? await callback(auth) : callback; 27 + 28 + if (!token) { 29 + return; 30 + } 31 + 32 + if (auth.scheme === 'bearer') { 33 + return `Bearer ${token}`; 34 + } 35 + 36 + if (auth.scheme === 'basic') { 37 + return `Basic ${btoa(token)}`; 38 + } 39 + 40 + return token; 41 + };
+84
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; 4 + 5 + export type QuerySerializer = (query: Record<string, unknown>) => string; 6 + 7 + export type BodySerializer = (body: any) => any; 8 + 9 + type QuerySerializerOptionsObject = { 10 + allowReserved?: boolean; 11 + array?: Partial<SerializerOptions<ArrayStyle>>; 12 + object?: Partial<SerializerOptions<ObjectStyle>>; 13 + }; 14 + 15 + export type QuerySerializerOptions = QuerySerializerOptionsObject & { 16 + /** 17 + * Per-parameter serialization overrides. When provided, these settings 18 + * override the global array/object settings for specific parameter names. 19 + */ 20 + parameters?: Record<string, QuerySerializerOptionsObject>; 21 + }; 22 + 23 + const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { 24 + if (typeof value === 'string' || value instanceof Blob) { 25 + data.append(key, value); 26 + } else if (value instanceof Date) { 27 + data.append(key, value.toISOString()); 28 + } else { 29 + data.append(key, JSON.stringify(value)); 30 + } 31 + }; 32 + 33 + const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { 34 + if (typeof value === 'string') { 35 + data.append(key, value); 36 + } else { 37 + data.append(key, JSON.stringify(value)); 38 + } 39 + }; 40 + 41 + export const formDataBodySerializer = { 42 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 43 + body: T, 44 + ): FormData => { 45 + const data = new FormData(); 46 + 47 + Object.entries(body).forEach(([key, value]) => { 48 + if (value === undefined || value === null) { 49 + return; 50 + } 51 + if (Array.isArray(value)) { 52 + value.forEach((v) => serializeFormDataPair(data, key, v)); 53 + } else { 54 + serializeFormDataPair(data, key, value); 55 + } 56 + }); 57 + 58 + return data; 59 + }, 60 + }; 61 + 62 + export const jsonBodySerializer = { 63 + bodySerializer: <T>(body: T): string => 64 + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), 65 + }; 66 + 67 + export const urlSearchParamsBodySerializer = { 68 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+169
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + type Slot = 'body' | 'headers' | 'path' | 'query'; 4 + 5 + export type Field = 6 + | { 7 + in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 11 + key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 16 + map?: string; 17 + } 18 + | { 19 + in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 23 + key?: string; 24 + map?: string; 25 + } 26 + | { 27 + /** 28 + * Field name. This is the name we want the user to see and use. 29 + */ 30 + key: string; 31 + /** 32 + * Field mapped name. This is the name we want to use in the request. 33 + * If `in` is omitted, `map` aliases `key` to the transport layer. 34 + */ 35 + map: Slot; 36 + }; 37 + 38 + export interface Fields { 39 + allowExtra?: Partial<Record<Slot, boolean>>; 40 + args?: ReadonlyArray<Field>; 41 + } 42 + 43 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 44 + 45 + const extraPrefixesMap: Record<string, Slot> = { 46 + $body_: 'body', 47 + $headers_: 'headers', 48 + $path_: 'path', 49 + $query_: 'query', 50 + }; 51 + const extraPrefixes = Object.entries(extraPrefixesMap); 52 + 53 + type KeyMap = Map< 54 + string, 55 + | { 56 + in: Slot; 57 + map?: string; 58 + } 59 + | { 60 + in?: never; 61 + map: Slot; 62 + } 63 + >; 64 + 65 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 66 + if (!map) { 67 + map = new Map(); 68 + } 69 + 70 + for (const config of fields) { 71 + if ('in' in config) { 72 + if (config.key) { 73 + map.set(config.key, { 74 + in: config.in, 75 + map: config.map, 76 + }); 77 + } 78 + } else if ('key' in config) { 79 + map.set(config.key, { 80 + map: config.map, 81 + }); 82 + } else if (config.args) { 83 + buildKeyMap(config.args, map); 84 + } 85 + } 86 + 87 + return map; 88 + }; 89 + 90 + interface Params { 91 + body: unknown; 92 + headers: Record<string, unknown>; 93 + path: Record<string, unknown>; 94 + query: Record<string, unknown>; 95 + } 96 + 97 + const stripEmptySlots = (params: Params) => { 98 + for (const [slot, value] of Object.entries(params)) { 99 + if (value && typeof value === 'object' && !Object.keys(value).length) { 100 + delete params[slot as Slot]; 101 + } 102 + } 103 + }; 104 + 105 + export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => { 106 + const params: Params = { 107 + body: {}, 108 + headers: {}, 109 + path: {}, 110 + query: {}, 111 + }; 112 + 113 + const map = buildKeyMap(fields); 114 + 115 + let config: FieldsConfig[number] | undefined; 116 + 117 + for (const [index, arg] of args.entries()) { 118 + if (fields[index]) { 119 + config = fields[index]; 120 + } 121 + 122 + if (!config) { 123 + continue; 124 + } 125 + 126 + if ('in' in config) { 127 + if (config.key) { 128 + const field = map.get(config.key)!; 129 + const name = field.map || config.key; 130 + if (field.in) { 131 + (params[field.in] as Record<string, unknown>)[name] = arg; 132 + } 133 + } else { 134 + params.body = arg; 135 + } 136 + } else { 137 + for (const [key, value] of Object.entries(arg ?? {})) { 138 + const field = map.get(key); 139 + 140 + if (field) { 141 + if (field.in) { 142 + const name = field.map || key; 143 + (params[field.in] as Record<string, unknown>)[name] = value; 144 + } else { 145 + params[field.map] = value; 146 + } 147 + } else { 148 + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); 149 + 150 + if (extra) { 151 + const [prefix, slot] = extra; 152 + (params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value; 153 + } else if ('allowExtra' in config && config.allowExtra) { 154 + for (const [slot, allowed] of Object.entries(config.allowExtra)) { 155 + if (allowed) { 156 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 157 + break; 158 + } 159 + } 160 + } 161 + } 162 + } 163 + } 164 + } 165 + 166 + stripEmptySlots(params); 167 + 168 + return params; 169 + };
+171
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; 107 + }; 108 + 109 + export const serializePrimitiveParam = ({ 110 + allowReserved, 111 + name, 112 + value, 113 + }: SerializePrimitiveParam) => { 114 + if (value === undefined || value === null) { 115 + return ''; 116 + } 117 + 118 + if (typeof value === 'object') { 119 + throw new Error( 120 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 121 + ); 122 + } 123 + 124 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 125 + }; 126 + 127 + export const serializeObjectParam = ({ 128 + allowReserved, 129 + explode, 130 + name, 131 + style, 132 + value, 133 + valueOnly, 134 + }: SerializeOptions<ObjectSeparatorStyle> & { 135 + value: Record<string, unknown> | Date; 136 + valueOnly?: boolean; 137 + }) => { 138 + if (value instanceof Date) { 139 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 140 + } 141 + 142 + if (style !== 'deepObject' && !explode) { 143 + let values: string[] = []; 144 + Object.entries(value).forEach(([key, v]) => { 145 + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; 146 + }); 147 + const joinedValues = values.join(','); 148 + switch (style) { 149 + case 'form': 150 + return `${name}=${joinedValues}`; 151 + case 'label': 152 + return `.${joinedValues}`; 153 + case 'matrix': 154 + return `;${name}=${joinedValues}`; 155 + default: 156 + return joinedValues; 157 + } 158 + } 159 + 160 + const separator = separatorObjectExplode(style); 161 + const joinedValues = Object.entries(value) 162 + .map(([key, v]) => 163 + serializePrimitiveParam({ 164 + allowReserved, 165 + name: style === 'deepObject' ? `${name}[${key}]` : key, 166 + value: v as string, 167 + }), 168 + ) 169 + .join(separator); 170 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; 171 + };
+117
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/queryKeySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + /** 4 + * JSON-friendly union that mirrors what Pinia Colada can hash. 5 + */ 6 + export type JsonValue = 7 + | null 8 + | string 9 + | number 10 + | boolean 11 + | JsonValue[] 12 + | { [key: string]: JsonValue }; 13 + 14 + /** 15 + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. 16 + */ 17 + export const queryKeyJsonReplacer = (_key: string, value: unknown) => { 18 + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { 19 + return undefined; 20 + } 21 + if (typeof value === 'bigint') { 22 + return value.toString(); 23 + } 24 + if (value instanceof Date) { 25 + return value.toISOString(); 26 + } 27 + return value; 28 + }; 29 + 30 + /** 31 + * Safely stringifies a value and parses it back into a JsonValue. 32 + */ 33 + export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { 34 + try { 35 + const json = JSON.stringify(input, queryKeyJsonReplacer); 36 + if (json === undefined) { 37 + return undefined; 38 + } 39 + return JSON.parse(json) as JsonValue; 40 + } catch { 41 + return undefined; 42 + } 43 + }; 44 + 45 + /** 46 + * Detects plain objects (including objects with a null prototype). 47 + */ 48 + const isPlainObject = (value: unknown): value is Record<string, unknown> => { 49 + if (value === null || typeof value !== 'object') { 50 + return false; 51 + } 52 + const prototype = Object.getPrototypeOf(value as object); 53 + return prototype === Object.prototype || prototype === null; 54 + }; 55 + 56 + /** 57 + * Turns URLSearchParams into a sorted JSON object for deterministic keys. 58 + */ 59 + const serializeSearchParams = (params: URLSearchParams): JsonValue => { 60 + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); 61 + const result: Record<string, JsonValue> = {}; 62 + 63 + for (const [key, value] of entries) { 64 + const existing = result[key]; 65 + if (existing === undefined) { 66 + result[key] = value; 67 + continue; 68 + } 69 + 70 + if (Array.isArray(existing)) { 71 + (existing as string[]).push(value); 72 + } else { 73 + result[key] = [existing, value]; 74 + } 75 + } 76 + 77 + return result; 78 + }; 79 + 80 + /** 81 + * Normalizes any accepted value into a JSON-friendly shape for query keys. 82 + */ 83 + export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { 84 + if (value === null) { 85 + return null; 86 + } 87 + 88 + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 89 + return value; 90 + } 91 + 92 + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { 93 + return undefined; 94 + } 95 + 96 + if (typeof value === 'bigint') { 97 + return value.toString(); 98 + } 99 + 100 + if (value instanceof Date) { 101 + return value.toISOString(); 102 + } 103 + 104 + if (Array.isArray(value)) { 105 + return stringifyToJsonValue(value); 106 + } 107 + 108 + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { 109 + return serializeSearchParams(value); 110 + } 111 + 112 + if (isPlainObject(value)) { 113 + return stringifyToJsonValue(value); 114 + } 115 + 116 + return undefined; 117 + };
+243
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> & 6 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 7 + /** 8 + * Fetch API implementation. You can use this option to provide a custom 9 + * fetch instance. 10 + * 11 + * @default globalThis.fetch 12 + */ 13 + fetch?: typeof fetch; 14 + /** 15 + * Implementing clients can call request interceptors inside this hook. 16 + */ 17 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 18 + /** 19 + * Callback invoked when a network or parsing error occurs during streaming. 20 + * 21 + * This option applies only if the endpoint returns a stream of events. 22 + * 23 + * @param error The error that occurred. 24 + */ 25 + onSseError?: (error: unknown) => void; 26 + /** 27 + * Callback invoked when an event is streamed from the server. 28 + * 29 + * This option applies only if the endpoint returns a stream of events. 30 + * 31 + * @param event Event streamed from the server. 32 + * @returns Nothing (void). 33 + */ 34 + onSseEvent?: (event: StreamEvent<TData>) => void; 35 + serializedBody?: RequestInit['body']; 36 + /** 37 + * Default retry delay in milliseconds. 38 + * 39 + * This option applies only if the endpoint returns a stream of events. 40 + * 41 + * @default 3000 42 + */ 43 + sseDefaultRetryDelay?: number; 44 + /** 45 + * Maximum number of retry attempts before giving up. 46 + */ 47 + sseMaxRetryAttempts?: number; 48 + /** 49 + * Maximum retry delay in milliseconds. 50 + * 51 + * Applies only when exponential backoff is used. 52 + * 53 + * This option applies only if the endpoint returns a stream of events. 54 + * 55 + * @default 30000 56 + */ 57 + sseMaxRetryDelay?: number; 58 + /** 59 + * Optional sleep function for retry backoff. 60 + * 61 + * Defaults to using `setTimeout`. 62 + */ 63 + sseSleepFn?: (ms: number) => Promise<void>; 64 + url: string; 65 + }; 66 + 67 + export interface StreamEvent<TData = unknown> { 68 + data: TData; 69 + event?: string; 70 + id?: string; 71 + retry?: number; 72 + } 73 + 74 + export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = { 75 + stream: AsyncGenerator< 76 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 77 + TReturn, 78 + TNext 79 + >; 80 + }; 81 + 82 + export const createSseClient = <TData = unknown>({ 83 + onRequest, 84 + onSseError, 85 + onSseEvent, 86 + responseTransformer, 87 + responseValidator, 88 + sseDefaultRetryDelay, 89 + sseMaxRetryAttempts, 90 + sseMaxRetryDelay, 91 + sseSleepFn, 92 + url, 93 + ...options 94 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 95 + let lastEventId: string | undefined; 96 + 97 + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 98 + 99 + const createStream = async function* () { 100 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 101 + let attempt = 0; 102 + const signal = options.signal ?? new AbortController().signal; 103 + 104 + while (true) { 105 + if (signal.aborted) break; 106 + 107 + attempt++; 108 + 109 + const headers = 110 + options.headers instanceof Headers 111 + ? options.headers 112 + : new Headers(options.headers as Record<string, string> | undefined); 113 + 114 + if (lastEventId !== undefined) { 115 + headers.set('Last-Event-ID', lastEventId); 116 + } 117 + 118 + try { 119 + const requestInit: RequestInit = { 120 + redirect: 'follow', 121 + ...options, 122 + body: options.serializedBody, 123 + headers, 124 + signal, 125 + }; 126 + let request = new Request(url, requestInit); 127 + if (onRequest) { 128 + request = await onRequest(url, requestInit); 129 + } 130 + // fetch must be assigned here, otherwise it would throw the error: 131 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 132 + const _fetch = options.fetch ?? globalThis.fetch; 133 + const response = await _fetch(request); 134 + 135 + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); 136 + 137 + if (!response.body) throw new Error('No body in SSE response'); 138 + 139 + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 140 + 141 + let buffer = ''; 142 + 143 + const abortHandler = () => { 144 + try { 145 + reader.cancel(); 146 + } catch { 147 + // noop 148 + } 149 + }; 150 + 151 + signal.addEventListener('abort', abortHandler); 152 + 153 + try { 154 + while (true) { 155 + const { done, value } = await reader.read(); 156 + if (done) break; 157 + buffer += value; 158 + // Normalize line endings: CRLF -> LF, then CR -> LF 159 + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); 160 + 161 + const chunks = buffer.split('\n\n'); 162 + buffer = chunks.pop() ?? ''; 163 + 164 + for (const chunk of chunks) { 165 + const lines = chunk.split('\n'); 166 + const dataLines: Array<string> = []; 167 + let eventName: string | undefined; 168 + 169 + for (const line of lines) { 170 + if (line.startsWith('data:')) { 171 + dataLines.push(line.replace(/^data:\s*/, '')); 172 + } else if (line.startsWith('event:')) { 173 + eventName = line.replace(/^event:\s*/, ''); 174 + } else if (line.startsWith('id:')) { 175 + lastEventId = line.replace(/^id:\s*/, ''); 176 + } else if (line.startsWith('retry:')) { 177 + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); 178 + if (!Number.isNaN(parsed)) { 179 + retryDelay = parsed; 180 + } 181 + } 182 + } 183 + 184 + let data: unknown; 185 + let parsedJson = false; 186 + 187 + if (dataLines.length) { 188 + const rawData = dataLines.join('\n'); 189 + try { 190 + data = JSON.parse(rawData); 191 + parsedJson = true; 192 + } catch { 193 + data = rawData; 194 + } 195 + } 196 + 197 + if (parsedJson) { 198 + if (responseValidator) { 199 + await responseValidator(data); 200 + } 201 + 202 + if (responseTransformer) { 203 + data = await responseTransformer(data); 204 + } 205 + } 206 + 207 + onSseEvent?.({ 208 + data, 209 + event: eventName, 210 + id: lastEventId, 211 + retry: retryDelay, 212 + }); 213 + 214 + if (dataLines.length) { 215 + yield data as any; 216 + } 217 + } 218 + } 219 + } finally { 220 + signal.removeEventListener('abort', abortHandler); 221 + reader.releaseLock(); 222 + } 223 + 224 + break; // exit loop on normal completion 225 + } catch (error) { 226 + // connection failed or aborted; retry after delay 227 + onSseError?.(error); 228 + 229 + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { 230 + break; // stop after firing error 231 + } 232 + 233 + // exponential backoff: double retry each attempt, cap at 30s 234 + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); 235 + await sleep(backoff); 236 + } 237 + } 238 + }; 239 + 240 + const stream = createStream(); 241 + 242 + return { stream }; 243 + };
+104
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 4 + import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; 5 + 6 + export type HttpMethod = 7 + | 'connect' 8 + | 'delete' 9 + | 'get' 10 + | 'head' 11 + | 'options' 12 + | 'patch' 13 + | 'post' 14 + | 'put' 15 + | 'trace'; 16 + 17 + export type Client< 18 + RequestFn = never, 19 + Config = unknown, 20 + MethodFn = never, 21 + BuildUrlFn = never, 22 + SseFn = never, 23 + > = { 24 + /** 25 + * Returns the final request URL. 26 + */ 27 + buildUrl: BuildUrlFn; 28 + getConfig: () => Config; 29 + request: RequestFn; 30 + setConfig: (config: Config) => Config; 31 + } & { 32 + [K in HttpMethod]: MethodFn; 33 + } & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); 34 + 35 + export interface Config { 36 + /** 37 + * Auth token or a function returning auth token. The resolved value will be 38 + * added to the request payload as defined by its `security` array. 39 + */ 40 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 41 + /** 42 + * A function for serializing request body parameter. By default, 43 + * {@link JSON.stringify()} will be used. 44 + */ 45 + bodySerializer?: BodySerializer | null; 46 + /** 47 + * An object containing any HTTP headers that you want to pre-populate your 48 + * `Headers` object with. 49 + * 50 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 51 + */ 52 + headers?: 53 + | RequestInit['headers'] 54 + | Record< 55 + string, 56 + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown 57 + >; 58 + /** 59 + * The request method. 60 + * 61 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 62 + */ 63 + method?: Uppercase<HttpMethod>; 64 + /** 65 + * A function for serializing request query parameters. By default, arrays 66 + * will be exploded in form style, objects will be exploded in deepObject 67 + * style, and reserved characters are percent-encoded. 68 + * 69 + * This method will have no effect if the native `paramsSerializer()` Axios 70 + * API function is used. 71 + * 72 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 73 + */ 74 + querySerializer?: QuerySerializer | QuerySerializerOptions; 75 + /** 76 + * A function validating request data. This is useful if you want to ensure 77 + * the request conforms to the desired shape, so it can be safely sent to 78 + * the server. 79 + */ 80 + requestValidator?: (data: unknown) => Promise<unknown>; 81 + /** 82 + * A function transforming response data before it's returned. This is useful 83 + * for post-processing data, e.g. converting ISO strings into Date objects. 84 + */ 85 + responseTransformer?: (data: unknown) => Promise<unknown>; 86 + /** 87 + * A function validating response data. This is useful if you want to ensure 88 + * the response conforms to the desired shape, so it can be safely passed to 89 + * the transformers and returned to the user. 90 + */ 91 + responseValidator?: (data: unknown) => Promise<unknown>; 92 + } 93 + 94 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 95 + ? true 96 + : [T] extends [never | undefined] 97 + ? [undefined] extends [T] 98 + ? false 99 + : true 100 + : false; 101 + 102 + export type OmitNever<T extends Record<string, unknown>> = { 103 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]; 104 + };
+140
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); 48 + continue; 49 + } 50 + 51 + if (typeof value === 'object') { 52 + url = url.replace( 53 + match, 54 + serializeObjectParam({ 55 + explode, 56 + name, 57 + style, 58 + value: value as Record<string, unknown>, 59 + valueOnly: true, 60 + }), 61 + ); 62 + continue; 63 + } 64 + 65 + if (style === 'matrix') { 66 + url = url.replace( 67 + match, 68 + `;${serializePrimitiveParam({ 69 + name, 70 + value: value as string, 71 + })}`, 72 + ); 73 + continue; 74 + } 75 + 76 + const replaceValue = encodeURIComponent( 77 + style === 'label' ? `.${value as string}` : (value as string), 78 + ); 79 + url = url.replace(match, replaceValue); 80 + } 81 + } 82 + return url; 83 + }; 84 + 85 + export const getUrl = ({ 86 + baseUrl, 87 + path, 88 + query, 89 + querySerializer, 90 + url: _url, 91 + }: { 92 + baseUrl?: string; 93 + path?: Record<string, unknown>; 94 + query?: Record<string, unknown>; 95 + querySerializer: QuerySerializer; 96 + url: string; 97 + }) => { 98 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 99 + let url = (baseUrl ?? '') + pathUrl; 100 + if (path) { 101 + url = defaultPathSerializer({ path, url }); 102 + } 103 + let search = query ? querySerializer(query) : ''; 104 + if (search.startsWith('?')) { 105 + search = search.substring(1); 106 + } 107 + if (search) { 108 + url += `?${search}`; 109 + } 110 + return url; 111 + }; 112 + 113 + export function getValidRequestBody(options: { 114 + body?: unknown; 115 + bodySerializer?: BodySerializer | null; 116 + serializedBody?: unknown; 117 + }) { 118 + const hasBody = options.body !== undefined; 119 + const isSerializedBody = hasBody && options.bodySerializer; 120 + 121 + if (isSerializedBody) { 122 + if ('serializedBody' in options) { 123 + const hasSerializedBody = 124 + options.serializedBody !== undefined && options.serializedBody !== ''; 125 + 126 + return hasSerializedBody ? options.serializedBody : null; 127 + } 128 + 129 + // not all clients implement a serializedBody property (i.e. client-axios) 130 + return options.body !== '' ? options.body : null; 131 + } 132 + 133 + // plain/text body 134 + if (hasBody) { 135 + return options.body; 136 + } 137 + 138 + // no body was provided 139 + return undefined; 140 + }
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { ClientOptions, CreateSecretData, CreateSecretResponse, CreateSecretResponses, ListSecretsData, ListSecretsResponse, ListSecretsResponses, PaginatedResponse, RepositorySecret } from './types.gen';
+21
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/transformers.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { CreateSecretResponse, ListSecretsResponse } from './types.gen'; 4 + 5 + const repositorySecretSchemaResponseTransformer = (data: any) => { 6 + data.createdAt = new Date(data.createdAt); 7 + data.updatedAt = new Date(data.updatedAt); 8 + return data; 9 + }; 10 + 11 + export const listSecretsResponseTransformer = async (data: any): Promise<ListSecretsResponse> => { 12 + if (data.data) { 13 + data.data = data.data.map((item: any) => repositorySecretSchemaResponseTransformer(item)); 14 + } 15 + return data; 16 + }; 17 + 18 + export const createSecretResponseTransformer = async (data: any): Promise<CreateSecretResponse> => { 19 + data = repositorySecretSchemaResponseTransformer(data); 20 + return data; 21 + };
+57
packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-allof-response-wrapper/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: '{protocol}://specs' | (string & {}); 5 + }; 6 + 7 + export type PaginatedResponse = { 8 + meta?: { 9 + page?: number; 10 + limit?: number; 11 + total?: number; 12 + totalPages?: number; 13 + }; 14 + }; 15 + 16 + export type RepositorySecret = { 17 + id: number; 18 + name: string; 19 + createdAt: Date; 20 + updatedAt: Date; 21 + }; 22 + 23 + export type ListSecretsData = { 24 + body?: never; 25 + path?: never; 26 + query?: never; 27 + url: '/secrets'; 28 + }; 29 + 30 + export type ListSecretsResponses = { 31 + /** 32 + * PaginatedResponseRepositorySecret 33 + * 34 + * OK 35 + */ 36 + 200: PaginatedResponse & { 37 + data?: Array<RepositorySecret>; 38 + }; 39 + }; 40 + 41 + export type ListSecretsResponse = ListSecretsResponses[keyof ListSecretsResponses]; 42 + 43 + export type CreateSecretData = { 44 + body?: never; 45 + path?: never; 46 + query?: never; 47 + url: '/secrets/{id}'; 48 + }; 49 + 50 + export type CreateSecretResponses = { 51 + /** 52 + * Created 53 + */ 54 + 201: RepositorySecret; 55 + }; 56 + 57 + export type CreateSecretResponse = CreateSecretResponses[keyof CreateSecretResponses];
+16
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { type ClientOptions, type Config, createClient, createConfig } from './client'; 4 + import type { ClientOptions as ClientOptions2 } from './types.gen'; 5 + 6 + /** 7 + * The `createClientConfig()` function will be called on client initialization 8 + * and the returned object will become the client's initial configuration. 9 + * 10 + * You may want to initialize your client this way instead of calling 11 + * `setConfig()`. This is useful for example if you're using Next.js 12 + * to ensure your client always has the correct values. 13 + */ 14 + export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>; 15 + 16 + export const client = createClient(createConfig<ClientOptions2>());
+288
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/client/client.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { createSseClient } from '../core/serverSentEvents.gen'; 4 + import type { HttpMethod } from '../core/types.gen'; 5 + import { getValidRequestBody } from '../core/utils.gen'; 6 + import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; 7 + import { 8 + buildUrl, 9 + createConfig, 10 + createInterceptors, 11 + getParseAs, 12 + mergeConfigs, 13 + mergeHeaders, 14 + setAuthParams, 15 + } from './utils.gen'; 16 + 17 + type ReqInit = Omit<RequestInit, 'body' | 'headers'> & { 18 + body?: any; 19 + headers: ReturnType<typeof mergeHeaders>; 20 + }; 21 + 22 + export const createClient = (config: Config = {}): Client => { 23 + let _config = mergeConfigs(createConfig(), config); 24 + 25 + const getConfig = (): Config => ({ ..._config }); 26 + 27 + const setConfig = (config: Config): Config => { 28 + _config = mergeConfigs(_config, config); 29 + return getConfig(); 30 + }; 31 + 32 + const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>(); 33 + 34 + const beforeRequest = async (options: RequestOptions) => { 35 + const opts = { 36 + ..._config, 37 + ...options, 38 + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, 39 + headers: mergeHeaders(_config.headers, options.headers), 40 + serializedBody: undefined, 41 + }; 42 + 43 + if (opts.security) { 44 + await setAuthParams({ 45 + ...opts, 46 + security: opts.security, 47 + }); 48 + } 49 + 50 + if (opts.requestValidator) { 51 + await opts.requestValidator(opts); 52 + } 53 + 54 + if (opts.body !== undefined && opts.bodySerializer) { 55 + opts.serializedBody = opts.bodySerializer(opts.body); 56 + } 57 + 58 + // remove Content-Type header if body is empty to avoid sending invalid requests 59 + if (opts.body === undefined || opts.serializedBody === '') { 60 + opts.headers.delete('Content-Type'); 61 + } 62 + 63 + const url = buildUrl(opts); 64 + 65 + return { opts, url }; 66 + }; 67 + 68 + const request: Client['request'] = async (options) => { 69 + // @ts-expect-error 70 + const { opts, url } = await beforeRequest(options); 71 + const requestInit: ReqInit = { 72 + redirect: 'follow', 73 + ...opts, 74 + body: getValidRequestBody(opts), 75 + }; 76 + 77 + let request = new Request(url, requestInit); 78 + 79 + for (const fn of interceptors.request.fns) { 80 + if (fn) { 81 + request = await fn(request, opts); 82 + } 83 + } 84 + 85 + // fetch must be assigned here, otherwise it would throw the error: 86 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 87 + const _fetch = opts.fetch!; 88 + let response: Response; 89 + 90 + try { 91 + response = await _fetch(request); 92 + } catch (error) { 93 + // Handle fetch exceptions (AbortError, network errors, etc.) 94 + let finalError = error; 95 + 96 + for (const fn of interceptors.error.fns) { 97 + if (fn) { 98 + finalError = (await fn(error, undefined as any, request, opts)) as unknown; 99 + } 100 + } 101 + 102 + finalError = finalError || ({} as unknown); 103 + 104 + if (opts.throwOnError) { 105 + throw finalError; 106 + } 107 + 108 + // Return error response 109 + return opts.responseStyle === 'data' 110 + ? undefined 111 + : { 112 + error: finalError, 113 + request, 114 + response: undefined as any, 115 + }; 116 + } 117 + 118 + for (const fn of interceptors.response.fns) { 119 + if (fn) { 120 + response = await fn(response, request, opts); 121 + } 122 + } 123 + 124 + const result = { 125 + request, 126 + response, 127 + }; 128 + 129 + if (response.ok) { 130 + const parseAs = 131 + (opts.parseAs === 'auto' 132 + ? getParseAs(response.headers.get('Content-Type')) 133 + : opts.parseAs) ?? 'json'; 134 + 135 + if (response.status === 204 || response.headers.get('Content-Length') === '0') { 136 + let emptyData: any; 137 + switch (parseAs) { 138 + case 'arrayBuffer': 139 + case 'blob': 140 + case 'text': 141 + emptyData = await response[parseAs](); 142 + break; 143 + case 'formData': 144 + emptyData = new FormData(); 145 + break; 146 + case 'stream': 147 + emptyData = response.body; 148 + break; 149 + case 'json': 150 + default: 151 + emptyData = {}; 152 + break; 153 + } 154 + return opts.responseStyle === 'data' 155 + ? emptyData 156 + : { 157 + data: emptyData, 158 + ...result, 159 + }; 160 + } 161 + 162 + let data: any; 163 + switch (parseAs) { 164 + case 'arrayBuffer': 165 + case 'blob': 166 + case 'formData': 167 + case 'text': 168 + data = await response[parseAs](); 169 + break; 170 + case 'json': { 171 + // Some servers return 200 with no Content-Length and empty body. 172 + // response.json() would throw; read as text and parse if non-empty. 173 + const text = await response.text(); 174 + data = text ? JSON.parse(text) : {}; 175 + break; 176 + } 177 + case 'stream': 178 + return opts.responseStyle === 'data' 179 + ? response.body 180 + : { 181 + data: response.body, 182 + ...result, 183 + }; 184 + } 185 + 186 + if (parseAs === 'json') { 187 + if (opts.responseValidator) { 188 + await opts.responseValidator(data); 189 + } 190 + 191 + if (opts.responseTransformer) { 192 + data = await opts.responseTransformer(data); 193 + } 194 + } 195 + 196 + return opts.responseStyle === 'data' 197 + ? data 198 + : { 199 + data, 200 + ...result, 201 + }; 202 + } 203 + 204 + const textError = await response.text(); 205 + let jsonError: unknown; 206 + 207 + try { 208 + jsonError = JSON.parse(textError); 209 + } catch { 210 + // noop 211 + } 212 + 213 + const error = jsonError ?? textError; 214 + let finalError = error; 215 + 216 + for (const fn of interceptors.error.fns) { 217 + if (fn) { 218 + finalError = (await fn(error, response, request, opts)) as string; 219 + } 220 + } 221 + 222 + finalError = finalError || ({} as string); 223 + 224 + if (opts.throwOnError) { 225 + throw finalError; 226 + } 227 + 228 + // TODO: we probably want to return error and improve types 229 + return opts.responseStyle === 'data' 230 + ? undefined 231 + : { 232 + error: finalError, 233 + ...result, 234 + }; 235 + }; 236 + 237 + const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => 238 + request({ ...options, method }); 239 + 240 + const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { 241 + const { opts, url } = await beforeRequest(options); 242 + return createSseClient({ 243 + ...opts, 244 + body: opts.body as BodyInit | null | undefined, 245 + headers: opts.headers as unknown as Record<string, string>, 246 + method, 247 + onRequest: async (url, init) => { 248 + let request = new Request(url, init); 249 + for (const fn of interceptors.request.fns) { 250 + if (fn) { 251 + request = await fn(request, opts); 252 + } 253 + } 254 + return request; 255 + }, 256 + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, 257 + url, 258 + }); 259 + }; 260 + 261 + return { 262 + buildUrl, 263 + connect: makeMethodFn('CONNECT'), 264 + delete: makeMethodFn('DELETE'), 265 + get: makeMethodFn('GET'), 266 + getConfig, 267 + head: makeMethodFn('HEAD'), 268 + interceptors, 269 + options: makeMethodFn('OPTIONS'), 270 + patch: makeMethodFn('PATCH'), 271 + post: makeMethodFn('POST'), 272 + put: makeMethodFn('PUT'), 273 + request, 274 + setConfig, 275 + sse: { 276 + connect: makeSseFn('CONNECT'), 277 + delete: makeSseFn('DELETE'), 278 + get: makeSseFn('GET'), 279 + head: makeSseFn('HEAD'), 280 + options: makeSseFn('OPTIONS'), 281 + patch: makeSseFn('PATCH'), 282 + post: makeSseFn('POST'), 283 + put: makeSseFn('PUT'), 284 + trace: makeSseFn('TRACE'), 285 + }, 286 + trace: makeMethodFn('TRACE'), 287 + } as Client; 288 + };
+25
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/client/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { Auth } from '../core/auth.gen'; 4 + export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + export { 6 + formDataBodySerializer, 7 + jsonBodySerializer, 8 + urlSearchParamsBodySerializer, 9 + } from '../core/bodySerializer.gen'; 10 + export { buildClientParams } from '../core/params.gen'; 11 + export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; 12 + export { createClient } from './client.gen'; 13 + export type { 14 + Client, 15 + ClientOptions, 16 + Config, 17 + CreateClientConfig, 18 + Options, 19 + RequestOptions, 20 + RequestResult, 21 + ResolvedRequestOptions, 22 + ResponseStyle, 23 + TDataShape, 24 + } from './types.gen'; 25 + export { createConfig, mergeHeaders } from './utils.gen';
+213
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/client/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth } from '../core/auth.gen'; 4 + import type { 5 + ServerSentEventsOptions, 6 + ServerSentEventsResult, 7 + } from '../core/serverSentEvents.gen'; 8 + import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; 9 + import type { Middleware } from './utils.gen'; 10 + 11 + export type ResponseStyle = 'data' | 'fields'; 12 + 13 + export interface Config<T extends ClientOptions = ClientOptions> 14 + extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig { 15 + /** 16 + * Base URL for all requests made by this client. 17 + */ 18 + baseUrl?: T['baseUrl']; 19 + /** 20 + * Fetch API implementation. You can use this option to provide a custom 21 + * fetch instance. 22 + * 23 + * @default globalThis.fetch 24 + */ 25 + fetch?: typeof fetch; 26 + /** 27 + * Please don't use the Fetch client for Next.js applications. The `next` 28 + * options won't have any effect. 29 + * 30 + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. 31 + */ 32 + next?: never; 33 + /** 34 + * Return the response data parsed in a specified format. By default, `auto` 35 + * will infer the appropriate method from the `Content-Type` response header. 36 + * You can override this behavior with any of the {@link Body} methods. 37 + * Select `stream` if you don't want to parse response data at all. 38 + * 39 + * @default 'auto' 40 + */ 41 + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; 42 + /** 43 + * Should we return only data or multiple fields (data, error, response, etc.)? 44 + * 45 + * @default 'fields' 46 + */ 47 + responseStyle?: ResponseStyle; 48 + /** 49 + * Throw an error instead of returning it in the response? 50 + * 51 + * @default false 52 + */ 53 + throwOnError?: T['throwOnError']; 54 + } 55 + 56 + export interface RequestOptions< 57 + TData = unknown, 58 + TResponseStyle extends ResponseStyle = 'fields', 59 + ThrowOnError extends boolean = boolean, 60 + Url extends string = string, 61 + > 62 + extends 63 + Config<{ 64 + responseStyle: TResponseStyle; 65 + throwOnError: ThrowOnError; 66 + }>, 67 + Pick< 68 + ServerSentEventsOptions<TData>, 69 + | 'onSseError' 70 + | 'onSseEvent' 71 + | 'sseDefaultRetryDelay' 72 + | 'sseMaxRetryAttempts' 73 + | 'sseMaxRetryDelay' 74 + > { 75 + /** 76 + * Any body that you want to add to your request. 77 + * 78 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} 79 + */ 80 + body?: unknown; 81 + path?: Record<string, unknown>; 82 + query?: Record<string, unknown>; 83 + /** 84 + * Security mechanism(s) to use for the request. 85 + */ 86 + security?: ReadonlyArray<Auth>; 87 + url: Url; 88 + } 89 + 90 + export interface ResolvedRequestOptions< 91 + TResponseStyle extends ResponseStyle = 'fields', 92 + ThrowOnError extends boolean = boolean, 93 + Url extends string = string, 94 + > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { 95 + serializedBody?: string; 96 + } 97 + 98 + export type RequestResult< 99 + TData = unknown, 100 + TError = unknown, 101 + ThrowOnError extends boolean = boolean, 102 + TResponseStyle extends ResponseStyle = 'fields', 103 + > = ThrowOnError extends true 104 + ? Promise< 105 + TResponseStyle extends 'data' 106 + ? TData extends Record<string, unknown> 107 + ? TData[keyof TData] 108 + : TData 109 + : { 110 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; 111 + request: Request; 112 + response: Response; 113 + } 114 + > 115 + : Promise< 116 + TResponseStyle extends 'data' 117 + ? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined 118 + : ( 119 + | { 120 + data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; 121 + error: undefined; 122 + } 123 + | { 124 + data: undefined; 125 + error: TError extends Record<string, unknown> ? TError[keyof TError] : TError; 126 + } 127 + ) & { 128 + request: Request; 129 + response: Response; 130 + } 131 + >; 132 + 133 + export interface ClientOptions { 134 + baseUrl?: string; 135 + responseStyle?: ResponseStyle; 136 + throwOnError?: boolean; 137 + } 138 + 139 + type MethodFn = < 140 + TData = unknown, 141 + TError = unknown, 142 + ThrowOnError extends boolean = false, 143 + TResponseStyle extends ResponseStyle = 'fields', 144 + >( 145 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 146 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 147 + 148 + type SseFn = < 149 + TData = unknown, 150 + TError = unknown, 151 + ThrowOnError extends boolean = false, 152 + TResponseStyle extends ResponseStyle = 'fields', 153 + >( 154 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>, 155 + ) => Promise<ServerSentEventsResult<TData, TError>>; 156 + 157 + type RequestFn = < 158 + TData = unknown, 159 + TError = unknown, 160 + ThrowOnError extends boolean = false, 161 + TResponseStyle extends ResponseStyle = 'fields', 162 + >( 163 + options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> & 164 + Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, 'method'>, 165 + ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 166 + 167 + type BuildUrlFn = < 168 + TData extends { 169 + body?: unknown; 170 + path?: Record<string, unknown>; 171 + query?: Record<string, unknown>; 172 + url: string; 173 + }, 174 + >( 175 + options: TData & Options<TData>, 176 + ) => string; 177 + 178 + export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & { 179 + interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; 180 + }; 181 + 182 + /** 183 + * The `createClientConfig()` function will be called on client initialization 184 + * and the returned object will become the client's initial configuration. 185 + * 186 + * You may want to initialize your client this way instead of calling 187 + * `setConfig()`. This is useful for example if you're using Next.js 188 + * to ensure your client always has the correct values. 189 + */ 190 + export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( 191 + override?: Config<ClientOptions & T>, 192 + ) => Config<Required<ClientOptions> & T>; 193 + 194 + export interface TDataShape { 195 + body?: unknown; 196 + headers?: unknown; 197 + path?: unknown; 198 + query?: unknown; 199 + url: string; 200 + } 201 + 202 + type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; 203 + 204 + export type Options< 205 + TData extends TDataShape = TDataShape, 206 + ThrowOnError extends boolean = boolean, 207 + TResponse = unknown, 208 + TResponseStyle extends ResponseStyle = 'fields', 209 + > = OmitKeys< 210 + RequestOptions<TResponse, TResponseStyle, ThrowOnError>, 211 + 'body' | 'path' | 'query' | 'url' 212 + > & 213 + ([TData] extends [never] ? unknown : Omit<TData, 'url'>);
+316
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/client/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import { getAuthToken } from '../core/auth.gen'; 4 + import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; 5 + import { jsonBodySerializer } from '../core/bodySerializer.gen'; 6 + import { 7 + serializeArrayParam, 8 + serializeObjectParam, 9 + serializePrimitiveParam, 10 + } from '../core/pathSerializer.gen'; 11 + import { getUrl } from '../core/utils.gen'; 12 + import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; 13 + 14 + export const createQuerySerializer = <T = unknown>({ 15 + parameters = {}, 16 + ...args 17 + }: QuerySerializerOptions = {}) => { 18 + const querySerializer = (queryParams: T) => { 19 + const search: string[] = []; 20 + if (queryParams && typeof queryParams === 'object') { 21 + for (const name in queryParams) { 22 + const value = queryParams[name]; 23 + 24 + if (value === undefined || value === null) { 25 + continue; 26 + } 27 + 28 + const options = parameters[name] || args; 29 + 30 + if (Array.isArray(value)) { 31 + const serializedArray = serializeArrayParam({ 32 + allowReserved: options.allowReserved, 33 + explode: true, 34 + name, 35 + style: 'form', 36 + value, 37 + ...options.array, 38 + }); 39 + if (serializedArray) search.push(serializedArray); 40 + } else if (typeof value === 'object') { 41 + const serializedObject = serializeObjectParam({ 42 + allowReserved: options.allowReserved, 43 + explode: true, 44 + name, 45 + style: 'deepObject', 46 + value: value as Record<string, unknown>, 47 + ...options.object, 48 + }); 49 + if (serializedObject) search.push(serializedObject); 50 + } else { 51 + const serializedPrimitive = serializePrimitiveParam({ 52 + allowReserved: options.allowReserved, 53 + name, 54 + value: value as string, 55 + }); 56 + if (serializedPrimitive) search.push(serializedPrimitive); 57 + } 58 + } 59 + } 60 + return search.join('&'); 61 + }; 62 + return querySerializer; 63 + }; 64 + 65 + /** 66 + * Infers parseAs value from provided Content-Type header. 67 + */ 68 + export const getParseAs = (contentType: string | null): Exclude<Config['parseAs'], 'auto'> => { 69 + if (!contentType) { 70 + // If no Content-Type header is provided, the best we can do is return the raw response body, 71 + // which is effectively the same as the 'stream' option. 72 + return 'stream'; 73 + } 74 + 75 + const cleanContent = contentType.split(';')[0]?.trim(); 76 + 77 + if (!cleanContent) { 78 + return; 79 + } 80 + 81 + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { 82 + return 'json'; 83 + } 84 + 85 + if (cleanContent === 'multipart/form-data') { 86 + return 'formData'; 87 + } 88 + 89 + if ( 90 + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) 91 + ) { 92 + return 'blob'; 93 + } 94 + 95 + if (cleanContent.startsWith('text/')) { 96 + return 'text'; 97 + } 98 + 99 + return; 100 + }; 101 + 102 + const checkForExistence = ( 103 + options: Pick<RequestOptions, 'auth' | 'query'> & { 104 + headers: Headers; 105 + }, 106 + name?: string, 107 + ): boolean => { 108 + if (!name) { 109 + return false; 110 + } 111 + if ( 112 + options.headers.has(name) || 113 + options.query?.[name] || 114 + options.headers.get('Cookie')?.includes(`${name}=`) 115 + ) { 116 + return true; 117 + } 118 + return false; 119 + }; 120 + 121 + export const setAuthParams = async ({ 122 + security, 123 + ...options 124 + }: Pick<Required<RequestOptions>, 'security'> & 125 + Pick<RequestOptions, 'auth' | 'query'> & { 126 + headers: Headers; 127 + }) => { 128 + for (const auth of security) { 129 + if (checkForExistence(options, auth.name)) { 130 + continue; 131 + } 132 + 133 + const token = await getAuthToken(auth, options.auth); 134 + 135 + if (!token) { 136 + continue; 137 + } 138 + 139 + const name = auth.name ?? 'Authorization'; 140 + 141 + switch (auth.in) { 142 + case 'query': 143 + if (!options.query) { 144 + options.query = {}; 145 + } 146 + options.query[name] = token; 147 + break; 148 + case 'cookie': 149 + options.headers.append('Cookie', `${name}=${token}`); 150 + break; 151 + case 'header': 152 + default: 153 + options.headers.set(name, token); 154 + break; 155 + } 156 + } 157 + }; 158 + 159 + export const buildUrl: Client['buildUrl'] = (options) => 160 + getUrl({ 161 + baseUrl: options.baseUrl as string, 162 + path: options.path, 163 + query: options.query, 164 + querySerializer: 165 + typeof options.querySerializer === 'function' 166 + ? options.querySerializer 167 + : createQuerySerializer(options.querySerializer), 168 + url: options.url, 169 + }); 170 + 171 + export const mergeConfigs = (a: Config, b: Config): Config => { 172 + const config = { ...a, ...b }; 173 + if (config.baseUrl?.endsWith('/')) { 174 + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); 175 + } 176 + config.headers = mergeHeaders(a.headers, b.headers); 177 + return config; 178 + }; 179 + 180 + const headersEntries = (headers: Headers): Array<[string, string]> => { 181 + const entries: Array<[string, string]> = []; 182 + headers.forEach((value, key) => { 183 + entries.push([key, value]); 184 + }); 185 + return entries; 186 + }; 187 + 188 + export const mergeHeaders = ( 189 + ...headers: Array<Required<Config>['headers'] | undefined> 190 + ): Headers => { 191 + const mergedHeaders = new Headers(); 192 + for (const header of headers) { 193 + if (!header) { 194 + continue; 195 + } 196 + 197 + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); 198 + 199 + for (const [key, value] of iterator) { 200 + if (value === null) { 201 + mergedHeaders.delete(key); 202 + } else if (Array.isArray(value)) { 203 + for (const v of value) { 204 + mergedHeaders.append(key, v as string); 205 + } 206 + } else if (value !== undefined) { 207 + // assume object headers are meant to be JSON stringified, i.e. their 208 + // content value in OpenAPI specification is 'application/json' 209 + mergedHeaders.set( 210 + key, 211 + typeof value === 'object' ? JSON.stringify(value) : (value as string), 212 + ); 213 + } 214 + } 215 + } 216 + return mergedHeaders; 217 + }; 218 + 219 + type ErrInterceptor<Err, Res, Req, Options> = ( 220 + error: Err, 221 + response: Res, 222 + request: Req, 223 + options: Options, 224 + ) => Err | Promise<Err>; 225 + 226 + type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>; 227 + 228 + type ResInterceptor<Res, Req, Options> = ( 229 + response: Res, 230 + request: Req, 231 + options: Options, 232 + ) => Res | Promise<Res>; 233 + 234 + class Interceptors<Interceptor> { 235 + fns: Array<Interceptor | null> = []; 236 + 237 + clear(): void { 238 + this.fns = []; 239 + } 240 + 241 + eject(id: number | Interceptor): void { 242 + const index = this.getInterceptorIndex(id); 243 + if (this.fns[index]) { 244 + this.fns[index] = null; 245 + } 246 + } 247 + 248 + exists(id: number | Interceptor): boolean { 249 + const index = this.getInterceptorIndex(id); 250 + return Boolean(this.fns[index]); 251 + } 252 + 253 + getInterceptorIndex(id: number | Interceptor): number { 254 + if (typeof id === 'number') { 255 + return this.fns[id] ? id : -1; 256 + } 257 + return this.fns.indexOf(id); 258 + } 259 + 260 + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { 261 + const index = this.getInterceptorIndex(id); 262 + if (this.fns[index]) { 263 + this.fns[index] = fn; 264 + return id; 265 + } 266 + return false; 267 + } 268 + 269 + use(fn: Interceptor): number { 270 + this.fns.push(fn); 271 + return this.fns.length - 1; 272 + } 273 + } 274 + 275 + export interface Middleware<Req, Res, Err, Options> { 276 + error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; 277 + request: Interceptors<ReqInterceptor<Req, Options>>; 278 + response: Interceptors<ResInterceptor<Res, Req, Options>>; 279 + } 280 + 281 + export const createInterceptors = <Req, Res, Err, Options>(): Middleware< 282 + Req, 283 + Res, 284 + Err, 285 + Options 286 + > => ({ 287 + error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), 288 + request: new Interceptors<ReqInterceptor<Req, Options>>(), 289 + response: new Interceptors<ResInterceptor<Res, Req, Options>>(), 290 + }); 291 + 292 + const defaultQuerySerializer = createQuerySerializer({ 293 + allowReserved: false, 294 + array: { 295 + explode: true, 296 + style: 'form', 297 + }, 298 + object: { 299 + explode: true, 300 + style: 'deepObject', 301 + }, 302 + }); 303 + 304 + const defaultHeaders = { 305 + 'Content-Type': 'application/json', 306 + }; 307 + 308 + export const createConfig = <T extends ClientOptions = ClientOptions>( 309 + override: Config<Omit<ClientOptions, keyof T> & T> = {}, 310 + ): Config<Omit<ClientOptions, keyof T> & T> => ({ 311 + ...jsonBodySerializer, 312 + headers: defaultHeaders, 313 + parseAs: 'auto', 314 + querySerializer: defaultQuerySerializer, 315 + ...override, 316 + });
+41
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/auth.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type AuthToken = string | undefined; 4 + 5 + export interface Auth { 6 + /** 7 + * Which part of the request do we use to send the auth? 8 + * 9 + * @default 'header' 10 + */ 11 + in?: 'header' | 'query' | 'cookie'; 12 + /** 13 + * Header or query parameter name. 14 + * 15 + * @default 'Authorization' 16 + */ 17 + name?: string; 18 + scheme?: 'basic' | 'bearer'; 19 + type: 'apiKey' | 'http'; 20 + } 21 + 22 + export const getAuthToken = async ( 23 + auth: Auth, 24 + callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, 25 + ): Promise<string | undefined> => { 26 + const token = typeof callback === 'function' ? await callback(auth) : callback; 27 + 28 + if (!token) { 29 + return; 30 + } 31 + 32 + if (auth.scheme === 'bearer') { 33 + return `Bearer ${token}`; 34 + } 35 + 36 + if (auth.scheme === 'basic') { 37 + return `Basic ${btoa(token)}`; 38 + } 39 + 40 + return token; 41 + };
+84
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/bodySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; 4 + 5 + export type QuerySerializer = (query: Record<string, unknown>) => string; 6 + 7 + export type BodySerializer = (body: any) => any; 8 + 9 + type QuerySerializerOptionsObject = { 10 + allowReserved?: boolean; 11 + array?: Partial<SerializerOptions<ArrayStyle>>; 12 + object?: Partial<SerializerOptions<ObjectStyle>>; 13 + }; 14 + 15 + export type QuerySerializerOptions = QuerySerializerOptionsObject & { 16 + /** 17 + * Per-parameter serialization overrides. When provided, these settings 18 + * override the global array/object settings for specific parameter names. 19 + */ 20 + parameters?: Record<string, QuerySerializerOptionsObject>; 21 + }; 22 + 23 + const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { 24 + if (typeof value === 'string' || value instanceof Blob) { 25 + data.append(key, value); 26 + } else if (value instanceof Date) { 27 + data.append(key, value.toISOString()); 28 + } else { 29 + data.append(key, JSON.stringify(value)); 30 + } 31 + }; 32 + 33 + const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { 34 + if (typeof value === 'string') { 35 + data.append(key, value); 36 + } else { 37 + data.append(key, JSON.stringify(value)); 38 + } 39 + }; 40 + 41 + export const formDataBodySerializer = { 42 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>( 43 + body: T, 44 + ): FormData => { 45 + const data = new FormData(); 46 + 47 + Object.entries(body).forEach(([key, value]) => { 48 + if (value === undefined || value === null) { 49 + return; 50 + } 51 + if (Array.isArray(value)) { 52 + value.forEach((v) => serializeFormDataPair(data, key, v)); 53 + } else { 54 + serializeFormDataPair(data, key, value); 55 + } 56 + }); 57 + 58 + return data; 59 + }, 60 + }; 61 + 62 + export const jsonBodySerializer = { 63 + bodySerializer: <T>(body: T): string => 64 + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), 65 + }; 66 + 67 + export const urlSearchParamsBodySerializer = { 68 + bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => { 69 + const data = new URLSearchParams(); 70 + 71 + Object.entries(body).forEach(([key, value]) => { 72 + if (value === undefined || value === null) { 73 + return; 74 + } 75 + if (Array.isArray(value)) { 76 + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); 77 + } else { 78 + serializeUrlSearchParamsPair(data, key, value); 79 + } 80 + }); 81 + 82 + return data.toString(); 83 + }, 84 + };
+169
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/params.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + type Slot = 'body' | 'headers' | 'path' | 'query'; 4 + 5 + export type Field = 6 + | { 7 + in: Exclude<Slot, 'body'>; 8 + /** 9 + * Field name. This is the name we want the user to see and use. 10 + */ 11 + key: string; 12 + /** 13 + * Field mapped name. This is the name we want to use in the request. 14 + * If omitted, we use the same value as `key`. 15 + */ 16 + map?: string; 17 + } 18 + | { 19 + in: Extract<Slot, 'body'>; 20 + /** 21 + * Key isn't required for bodies. 22 + */ 23 + key?: string; 24 + map?: string; 25 + } 26 + | { 27 + /** 28 + * Field name. This is the name we want the user to see and use. 29 + */ 30 + key: string; 31 + /** 32 + * Field mapped name. This is the name we want to use in the request. 33 + * If `in` is omitted, `map` aliases `key` to the transport layer. 34 + */ 35 + map: Slot; 36 + }; 37 + 38 + export interface Fields { 39 + allowExtra?: Partial<Record<Slot, boolean>>; 40 + args?: ReadonlyArray<Field>; 41 + } 42 + 43 + export type FieldsConfig = ReadonlyArray<Field | Fields>; 44 + 45 + const extraPrefixesMap: Record<string, Slot> = { 46 + $body_: 'body', 47 + $headers_: 'headers', 48 + $path_: 'path', 49 + $query_: 'query', 50 + }; 51 + const extraPrefixes = Object.entries(extraPrefixesMap); 52 + 53 + type KeyMap = Map< 54 + string, 55 + | { 56 + in: Slot; 57 + map?: string; 58 + } 59 + | { 60 + in?: never; 61 + map: Slot; 62 + } 63 + >; 64 + 65 + const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { 66 + if (!map) { 67 + map = new Map(); 68 + } 69 + 70 + for (const config of fields) { 71 + if ('in' in config) { 72 + if (config.key) { 73 + map.set(config.key, { 74 + in: config.in, 75 + map: config.map, 76 + }); 77 + } 78 + } else if ('key' in config) { 79 + map.set(config.key, { 80 + map: config.map, 81 + }); 82 + } else if (config.args) { 83 + buildKeyMap(config.args, map); 84 + } 85 + } 86 + 87 + return map; 88 + }; 89 + 90 + interface Params { 91 + body: unknown; 92 + headers: Record<string, unknown>; 93 + path: Record<string, unknown>; 94 + query: Record<string, unknown>; 95 + } 96 + 97 + const stripEmptySlots = (params: Params) => { 98 + for (const [slot, value] of Object.entries(params)) { 99 + if (value && typeof value === 'object' && !Object.keys(value).length) { 100 + delete params[slot as Slot]; 101 + } 102 + } 103 + }; 104 + 105 + export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => { 106 + const params: Params = { 107 + body: {}, 108 + headers: {}, 109 + path: {}, 110 + query: {}, 111 + }; 112 + 113 + const map = buildKeyMap(fields); 114 + 115 + let config: FieldsConfig[number] | undefined; 116 + 117 + for (const [index, arg] of args.entries()) { 118 + if (fields[index]) { 119 + config = fields[index]; 120 + } 121 + 122 + if (!config) { 123 + continue; 124 + } 125 + 126 + if ('in' in config) { 127 + if (config.key) { 128 + const field = map.get(config.key)!; 129 + const name = field.map || config.key; 130 + if (field.in) { 131 + (params[field.in] as Record<string, unknown>)[name] = arg; 132 + } 133 + } else { 134 + params.body = arg; 135 + } 136 + } else { 137 + for (const [key, value] of Object.entries(arg ?? {})) { 138 + const field = map.get(key); 139 + 140 + if (field) { 141 + if (field.in) { 142 + const name = field.map || key; 143 + (params[field.in] as Record<string, unknown>)[name] = value; 144 + } else { 145 + params[field.map] = value; 146 + } 147 + } else { 148 + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); 149 + 150 + if (extra) { 151 + const [prefix, slot] = extra; 152 + (params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value; 153 + } else if ('allowExtra' in config && config.allowExtra) { 154 + for (const [slot, allowed] of Object.entries(config.allowExtra)) { 155 + if (allowed) { 156 + (params[slot as Slot] as Record<string, unknown>)[key] = value; 157 + break; 158 + } 159 + } 160 + } 161 + } 162 + } 163 + } 164 + } 165 + 166 + stripEmptySlots(params); 167 + 168 + return params; 169 + };
+171
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/pathSerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {} 4 + 5 + interface SerializePrimitiveOptions { 6 + allowReserved?: boolean; 7 + name: string; 8 + } 9 + 10 + export interface SerializerOptions<T> { 11 + /** 12 + * @default true 13 + */ 14 + explode: boolean; 15 + style: T; 16 + } 17 + 18 + export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; 19 + export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; 20 + type MatrixStyle = 'label' | 'matrix' | 'simple'; 21 + export type ObjectStyle = 'form' | 'deepObject'; 22 + type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; 23 + 24 + interface SerializePrimitiveParam extends SerializePrimitiveOptions { 25 + value: string; 26 + } 27 + 28 + export const separatorArrayExplode = (style: ArraySeparatorStyle) => { 29 + switch (style) { 30 + case 'label': 31 + return '.'; 32 + case 'matrix': 33 + return ';'; 34 + case 'simple': 35 + return ','; 36 + default: 37 + return '&'; 38 + } 39 + }; 40 + 41 + export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { 42 + switch (style) { 43 + case 'form': 44 + return ','; 45 + case 'pipeDelimited': 46 + return '|'; 47 + case 'spaceDelimited': 48 + return '%20'; 49 + default: 50 + return ','; 51 + } 52 + }; 53 + 54 + export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { 55 + switch (style) { 56 + case 'label': 57 + return '.'; 58 + case 'matrix': 59 + return ';'; 60 + case 'simple': 61 + return ','; 62 + default: 63 + return '&'; 64 + } 65 + }; 66 + 67 + export const serializeArrayParam = ({ 68 + allowReserved, 69 + explode, 70 + name, 71 + style, 72 + value, 73 + }: SerializeOptions<ArraySeparatorStyle> & { 74 + value: unknown[]; 75 + }) => { 76 + if (!explode) { 77 + const joinedValues = ( 78 + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) 79 + ).join(separatorArrayNoExplode(style)); 80 + switch (style) { 81 + case 'label': 82 + return `.${joinedValues}`; 83 + case 'matrix': 84 + return `;${name}=${joinedValues}`; 85 + case 'simple': 86 + return joinedValues; 87 + default: 88 + return `${name}=${joinedValues}`; 89 + } 90 + } 91 + 92 + const separator = separatorArrayExplode(style); 93 + const joinedValues = value 94 + .map((v) => { 95 + if (style === 'label' || style === 'simple') { 96 + return allowReserved ? v : encodeURIComponent(v as string); 97 + } 98 + 99 + return serializePrimitiveParam({ 100 + allowReserved, 101 + name, 102 + value: v as string, 103 + }); 104 + }) 105 + .join(separator); 106 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; 107 + }; 108 + 109 + export const serializePrimitiveParam = ({ 110 + allowReserved, 111 + name, 112 + value, 113 + }: SerializePrimitiveParam) => { 114 + if (value === undefined || value === null) { 115 + return ''; 116 + } 117 + 118 + if (typeof value === 'object') { 119 + throw new Error( 120 + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', 121 + ); 122 + } 123 + 124 + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; 125 + }; 126 + 127 + export const serializeObjectParam = ({ 128 + allowReserved, 129 + explode, 130 + name, 131 + style, 132 + value, 133 + valueOnly, 134 + }: SerializeOptions<ObjectSeparatorStyle> & { 135 + value: Record<string, unknown> | Date; 136 + valueOnly?: boolean; 137 + }) => { 138 + if (value instanceof Date) { 139 + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; 140 + } 141 + 142 + if (style !== 'deepObject' && !explode) { 143 + let values: string[] = []; 144 + Object.entries(value).forEach(([key, v]) => { 145 + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; 146 + }); 147 + const joinedValues = values.join(','); 148 + switch (style) { 149 + case 'form': 150 + return `${name}=${joinedValues}`; 151 + case 'label': 152 + return `.${joinedValues}`; 153 + case 'matrix': 154 + return `;${name}=${joinedValues}`; 155 + default: 156 + return joinedValues; 157 + } 158 + } 159 + 160 + const separator = separatorObjectExplode(style); 161 + const joinedValues = Object.entries(value) 162 + .map(([key, v]) => 163 + serializePrimitiveParam({ 164 + allowReserved, 165 + name: style === 'deepObject' ? `${name}[${key}]` : key, 166 + value: v as string, 167 + }), 168 + ) 169 + .join(separator); 170 + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; 171 + };
+117
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/queryKeySerializer.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + /** 4 + * JSON-friendly union that mirrors what Pinia Colada can hash. 5 + */ 6 + export type JsonValue = 7 + | null 8 + | string 9 + | number 10 + | boolean 11 + | JsonValue[] 12 + | { [key: string]: JsonValue }; 13 + 14 + /** 15 + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. 16 + */ 17 + export const queryKeyJsonReplacer = (_key: string, value: unknown) => { 18 + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { 19 + return undefined; 20 + } 21 + if (typeof value === 'bigint') { 22 + return value.toString(); 23 + } 24 + if (value instanceof Date) { 25 + return value.toISOString(); 26 + } 27 + return value; 28 + }; 29 + 30 + /** 31 + * Safely stringifies a value and parses it back into a JsonValue. 32 + */ 33 + export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { 34 + try { 35 + const json = JSON.stringify(input, queryKeyJsonReplacer); 36 + if (json === undefined) { 37 + return undefined; 38 + } 39 + return JSON.parse(json) as JsonValue; 40 + } catch { 41 + return undefined; 42 + } 43 + }; 44 + 45 + /** 46 + * Detects plain objects (including objects with a null prototype). 47 + */ 48 + const isPlainObject = (value: unknown): value is Record<string, unknown> => { 49 + if (value === null || typeof value !== 'object') { 50 + return false; 51 + } 52 + const prototype = Object.getPrototypeOf(value as object); 53 + return prototype === Object.prototype || prototype === null; 54 + }; 55 + 56 + /** 57 + * Turns URLSearchParams into a sorted JSON object for deterministic keys. 58 + */ 59 + const serializeSearchParams = (params: URLSearchParams): JsonValue => { 60 + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); 61 + const result: Record<string, JsonValue> = {}; 62 + 63 + for (const [key, value] of entries) { 64 + const existing = result[key]; 65 + if (existing === undefined) { 66 + result[key] = value; 67 + continue; 68 + } 69 + 70 + if (Array.isArray(existing)) { 71 + (existing as string[]).push(value); 72 + } else { 73 + result[key] = [existing, value]; 74 + } 75 + } 76 + 77 + return result; 78 + }; 79 + 80 + /** 81 + * Normalizes any accepted value into a JSON-friendly shape for query keys. 82 + */ 83 + export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { 84 + if (value === null) { 85 + return null; 86 + } 87 + 88 + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 89 + return value; 90 + } 91 + 92 + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { 93 + return undefined; 94 + } 95 + 96 + if (typeof value === 'bigint') { 97 + return value.toString(); 98 + } 99 + 100 + if (value instanceof Date) { 101 + return value.toISOString(); 102 + } 103 + 104 + if (Array.isArray(value)) { 105 + return stringifyToJsonValue(value); 106 + } 107 + 108 + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { 109 + return serializeSearchParams(value); 110 + } 111 + 112 + if (isPlainObject(value)) { 113 + return stringifyToJsonValue(value); 114 + } 115 + 116 + return undefined; 117 + };
+243
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/serverSentEvents.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Config } from './types.gen'; 4 + 5 + export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> & 6 + Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & { 7 + /** 8 + * Fetch API implementation. You can use this option to provide a custom 9 + * fetch instance. 10 + * 11 + * @default globalThis.fetch 12 + */ 13 + fetch?: typeof fetch; 14 + /** 15 + * Implementing clients can call request interceptors inside this hook. 16 + */ 17 + onRequest?: (url: string, init: RequestInit) => Promise<Request>; 18 + /** 19 + * Callback invoked when a network or parsing error occurs during streaming. 20 + * 21 + * This option applies only if the endpoint returns a stream of events. 22 + * 23 + * @param error The error that occurred. 24 + */ 25 + onSseError?: (error: unknown) => void; 26 + /** 27 + * Callback invoked when an event is streamed from the server. 28 + * 29 + * This option applies only if the endpoint returns a stream of events. 30 + * 31 + * @param event Event streamed from the server. 32 + * @returns Nothing (void). 33 + */ 34 + onSseEvent?: (event: StreamEvent<TData>) => void; 35 + serializedBody?: RequestInit['body']; 36 + /** 37 + * Default retry delay in milliseconds. 38 + * 39 + * This option applies only if the endpoint returns a stream of events. 40 + * 41 + * @default 3000 42 + */ 43 + sseDefaultRetryDelay?: number; 44 + /** 45 + * Maximum number of retry attempts before giving up. 46 + */ 47 + sseMaxRetryAttempts?: number; 48 + /** 49 + * Maximum retry delay in milliseconds. 50 + * 51 + * Applies only when exponential backoff is used. 52 + * 53 + * This option applies only if the endpoint returns a stream of events. 54 + * 55 + * @default 30000 56 + */ 57 + sseMaxRetryDelay?: number; 58 + /** 59 + * Optional sleep function for retry backoff. 60 + * 61 + * Defaults to using `setTimeout`. 62 + */ 63 + sseSleepFn?: (ms: number) => Promise<void>; 64 + url: string; 65 + }; 66 + 67 + export interface StreamEvent<TData = unknown> { 68 + data: TData; 69 + event?: string; 70 + id?: string; 71 + retry?: number; 72 + } 73 + 74 + export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = { 75 + stream: AsyncGenerator< 76 + TData extends Record<string, unknown> ? TData[keyof TData] : TData, 77 + TReturn, 78 + TNext 79 + >; 80 + }; 81 + 82 + export const createSseClient = <TData = unknown>({ 83 + onRequest, 84 + onSseError, 85 + onSseEvent, 86 + responseTransformer, 87 + responseValidator, 88 + sseDefaultRetryDelay, 89 + sseMaxRetryAttempts, 90 + sseMaxRetryDelay, 91 + sseSleepFn, 92 + url, 93 + ...options 94 + }: ServerSentEventsOptions): ServerSentEventsResult<TData> => { 95 + let lastEventId: string | undefined; 96 + 97 + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); 98 + 99 + const createStream = async function* () { 100 + let retryDelay: number = sseDefaultRetryDelay ?? 3000; 101 + let attempt = 0; 102 + const signal = options.signal ?? new AbortController().signal; 103 + 104 + while (true) { 105 + if (signal.aborted) break; 106 + 107 + attempt++; 108 + 109 + const headers = 110 + options.headers instanceof Headers 111 + ? options.headers 112 + : new Headers(options.headers as Record<string, string> | undefined); 113 + 114 + if (lastEventId !== undefined) { 115 + headers.set('Last-Event-ID', lastEventId); 116 + } 117 + 118 + try { 119 + const requestInit: RequestInit = { 120 + redirect: 'follow', 121 + ...options, 122 + body: options.serializedBody, 123 + headers, 124 + signal, 125 + }; 126 + let request = new Request(url, requestInit); 127 + if (onRequest) { 128 + request = await onRequest(url, requestInit); 129 + } 130 + // fetch must be assigned here, otherwise it would throw the error: 131 + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation 132 + const _fetch = options.fetch ?? globalThis.fetch; 133 + const response = await _fetch(request); 134 + 135 + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); 136 + 137 + if (!response.body) throw new Error('No body in SSE response'); 138 + 139 + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 140 + 141 + let buffer = ''; 142 + 143 + const abortHandler = () => { 144 + try { 145 + reader.cancel(); 146 + } catch { 147 + // noop 148 + } 149 + }; 150 + 151 + signal.addEventListener('abort', abortHandler); 152 + 153 + try { 154 + while (true) { 155 + const { done, value } = await reader.read(); 156 + if (done) break; 157 + buffer += value; 158 + // Normalize line endings: CRLF -> LF, then CR -> LF 159 + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); 160 + 161 + const chunks = buffer.split('\n\n'); 162 + buffer = chunks.pop() ?? ''; 163 + 164 + for (const chunk of chunks) { 165 + const lines = chunk.split('\n'); 166 + const dataLines: Array<string> = []; 167 + let eventName: string | undefined; 168 + 169 + for (const line of lines) { 170 + if (line.startsWith('data:')) { 171 + dataLines.push(line.replace(/^data:\s*/, '')); 172 + } else if (line.startsWith('event:')) { 173 + eventName = line.replace(/^event:\s*/, ''); 174 + } else if (line.startsWith('id:')) { 175 + lastEventId = line.replace(/^id:\s*/, ''); 176 + } else if (line.startsWith('retry:')) { 177 + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); 178 + if (!Number.isNaN(parsed)) { 179 + retryDelay = parsed; 180 + } 181 + } 182 + } 183 + 184 + let data: unknown; 185 + let parsedJson = false; 186 + 187 + if (dataLines.length) { 188 + const rawData = dataLines.join('\n'); 189 + try { 190 + data = JSON.parse(rawData); 191 + parsedJson = true; 192 + } catch { 193 + data = rawData; 194 + } 195 + } 196 + 197 + if (parsedJson) { 198 + if (responseValidator) { 199 + await responseValidator(data); 200 + } 201 + 202 + if (responseTransformer) { 203 + data = await responseTransformer(data); 204 + } 205 + } 206 + 207 + onSseEvent?.({ 208 + data, 209 + event: eventName, 210 + id: lastEventId, 211 + retry: retryDelay, 212 + }); 213 + 214 + if (dataLines.length) { 215 + yield data as any; 216 + } 217 + } 218 + } 219 + } finally { 220 + signal.removeEventListener('abort', abortHandler); 221 + reader.releaseLock(); 222 + } 223 + 224 + break; // exit loop on normal completion 225 + } catch (error) { 226 + // connection failed or aborted; retry after delay 227 + onSseError?.(error); 228 + 229 + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { 230 + break; // stop after firing error 231 + } 232 + 233 + // exponential backoff: double retry each attempt, cap at 30s 234 + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); 235 + await sleep(backoff); 236 + } 237 + } 238 + }; 239 + 240 + const stream = createStream(); 241 + 242 + return { stream }; 243 + };
+104
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { Auth, AuthToken } from './auth.gen'; 4 + import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; 5 + 6 + export type HttpMethod = 7 + | 'connect' 8 + | 'delete' 9 + | 'get' 10 + | 'head' 11 + | 'options' 12 + | 'patch' 13 + | 'post' 14 + | 'put' 15 + | 'trace'; 16 + 17 + export type Client< 18 + RequestFn = never, 19 + Config = unknown, 20 + MethodFn = never, 21 + BuildUrlFn = never, 22 + SseFn = never, 23 + > = { 24 + /** 25 + * Returns the final request URL. 26 + */ 27 + buildUrl: BuildUrlFn; 28 + getConfig: () => Config; 29 + request: RequestFn; 30 + setConfig: (config: Config) => Config; 31 + } & { 32 + [K in HttpMethod]: MethodFn; 33 + } & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); 34 + 35 + export interface Config { 36 + /** 37 + * Auth token or a function returning auth token. The resolved value will be 38 + * added to the request payload as defined by its `security` array. 39 + */ 40 + auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; 41 + /** 42 + * A function for serializing request body parameter. By default, 43 + * {@link JSON.stringify()} will be used. 44 + */ 45 + bodySerializer?: BodySerializer | null; 46 + /** 47 + * An object containing any HTTP headers that you want to pre-populate your 48 + * `Headers` object with. 49 + * 50 + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} 51 + */ 52 + headers?: 53 + | RequestInit['headers'] 54 + | Record< 55 + string, 56 + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown 57 + >; 58 + /** 59 + * The request method. 60 + * 61 + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} 62 + */ 63 + method?: Uppercase<HttpMethod>; 64 + /** 65 + * A function for serializing request query parameters. By default, arrays 66 + * will be exploded in form style, objects will be exploded in deepObject 67 + * style, and reserved characters are percent-encoded. 68 + * 69 + * This method will have no effect if the native `paramsSerializer()` Axios 70 + * API function is used. 71 + * 72 + * {@link https://swagger.io/docs/specification/serialization/#query View examples} 73 + */ 74 + querySerializer?: QuerySerializer | QuerySerializerOptions; 75 + /** 76 + * A function validating request data. This is useful if you want to ensure 77 + * the request conforms to the desired shape, so it can be safely sent to 78 + * the server. 79 + */ 80 + requestValidator?: (data: unknown) => Promise<unknown>; 81 + /** 82 + * A function transforming response data before it's returned. This is useful 83 + * for post-processing data, e.g. converting ISO strings into Date objects. 84 + */ 85 + responseTransformer?: (data: unknown) => Promise<unknown>; 86 + /** 87 + * A function validating response data. This is useful if you want to ensure 88 + * the response conforms to the desired shape, so it can be safely passed to 89 + * the transformers and returned to the user. 90 + */ 91 + responseValidator?: (data: unknown) => Promise<unknown>; 92 + } 93 + 94 + type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] 95 + ? true 96 + : [T] extends [never | undefined] 97 + ? [undefined] extends [T] 98 + ? false 99 + : true 100 + : false; 101 + 102 + export type OmitNever<T extends Record<string, unknown>> = { 103 + [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]; 104 + };
+140
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/core/utils.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; 4 + import { 5 + type ArraySeparatorStyle, 6 + serializeArrayParam, 7 + serializeObjectParam, 8 + serializePrimitiveParam, 9 + } from './pathSerializer.gen'; 10 + 11 + export interface PathSerializer { 12 + path: Record<string, unknown>; 13 + url: string; 14 + } 15 + 16 + export const PATH_PARAM_RE = /\{[^{}]+\}/g; 17 + 18 + export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { 19 + let url = _url; 20 + const matches = _url.match(PATH_PARAM_RE); 21 + if (matches) { 22 + for (const match of matches) { 23 + let explode = false; 24 + let name = match.substring(1, match.length - 1); 25 + let style: ArraySeparatorStyle = 'simple'; 26 + 27 + if (name.endsWith('*')) { 28 + explode = true; 29 + name = name.substring(0, name.length - 1); 30 + } 31 + 32 + if (name.startsWith('.')) { 33 + name = name.substring(1); 34 + style = 'label'; 35 + } else if (name.startsWith(';')) { 36 + name = name.substring(1); 37 + style = 'matrix'; 38 + } 39 + 40 + const value = path[name]; 41 + 42 + if (value === undefined || value === null) { 43 + continue; 44 + } 45 + 46 + if (Array.isArray(value)) { 47 + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); 48 + continue; 49 + } 50 + 51 + if (typeof value === 'object') { 52 + url = url.replace( 53 + match, 54 + serializeObjectParam({ 55 + explode, 56 + name, 57 + style, 58 + value: value as Record<string, unknown>, 59 + valueOnly: true, 60 + }), 61 + ); 62 + continue; 63 + } 64 + 65 + if (style === 'matrix') { 66 + url = url.replace( 67 + match, 68 + `;${serializePrimitiveParam({ 69 + name, 70 + value: value as string, 71 + })}`, 72 + ); 73 + continue; 74 + } 75 + 76 + const replaceValue = encodeURIComponent( 77 + style === 'label' ? `.${value as string}` : (value as string), 78 + ); 79 + url = url.replace(match, replaceValue); 80 + } 81 + } 82 + return url; 83 + }; 84 + 85 + export const getUrl = ({ 86 + baseUrl, 87 + path, 88 + query, 89 + querySerializer, 90 + url: _url, 91 + }: { 92 + baseUrl?: string; 93 + path?: Record<string, unknown>; 94 + query?: Record<string, unknown>; 95 + querySerializer: QuerySerializer; 96 + url: string; 97 + }) => { 98 + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; 99 + let url = (baseUrl ?? '') + pathUrl; 100 + if (path) { 101 + url = defaultPathSerializer({ path, url }); 102 + } 103 + let search = query ? querySerializer(query) : ''; 104 + if (search.startsWith('?')) { 105 + search = search.substring(1); 106 + } 107 + if (search) { 108 + url += `?${search}`; 109 + } 110 + return url; 111 + }; 112 + 113 + export function getValidRequestBody(options: { 114 + body?: unknown; 115 + bodySerializer?: BodySerializer | null; 116 + serializedBody?: unknown; 117 + }) { 118 + const hasBody = options.body !== undefined; 119 + const isSerializedBody = hasBody && options.bodySerializer; 120 + 121 + if (isSerializedBody) { 122 + if ('serializedBody' in options) { 123 + const hasSerializedBody = 124 + options.serializedBody !== undefined && options.serializedBody !== ''; 125 + 126 + return hasSerializedBody ? options.serializedBody : null; 127 + } 128 + 129 + // not all clients implement a serializedBody property (i.e. client-axios) 130 + return options.body !== '' ? options.body : null; 131 + } 132 + 133 + // plain/text body 134 + if (hasBody) { 135 + return options.body; 136 + } 137 + 138 + // no body was provided 139 + return undefined; 140 + }
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type { ClientOptions, CreateSecretData, CreateSecretResponse, CreateSecretResponses, ListSecretsData, ListSecretsResponse, ListSecretsResponses, PaginatedResponse, RepositorySecret } from './types.gen';
+21
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/transformers.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { CreateSecretResponse, ListSecretsResponse } from './types.gen'; 4 + 5 + const repositorySecretSchemaResponseTransformer = (data: any) => { 6 + data.createdAt = new Date(data.createdAt); 7 + data.updatedAt = new Date(data.updatedAt); 8 + return data; 9 + }; 10 + 11 + export const listSecretsResponseTransformer = async (data: any): Promise<ListSecretsResponse> => { 12 + if (data.data) { 13 + data.data = data.data.map((item: any) => repositorySecretSchemaResponseTransformer(item)); 14 + } 15 + return data; 16 + }; 17 + 18 + export const createSecretResponseTransformer = async (data: any): Promise<CreateSecretResponse> => { 19 + data = repositorySecretSchemaResponseTransformer(data); 20 + return data; 21 + };
+57
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-allof-response-wrapper/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: '{protocol}://specs' | (string & {}); 5 + }; 6 + 7 + export type PaginatedResponse = { 8 + meta?: { 9 + page?: number; 10 + limit?: number; 11 + total?: number; 12 + totalPages?: number; 13 + }; 14 + }; 15 + 16 + export type RepositorySecret = { 17 + id: number; 18 + name: string; 19 + createdAt: Date; 20 + updatedAt: Date; 21 + }; 22 + 23 + export type ListSecretsData = { 24 + body?: never; 25 + path?: never; 26 + query?: never; 27 + url: '/secrets'; 28 + }; 29 + 30 + export type ListSecretsResponses = { 31 + /** 32 + * PaginatedResponseRepositorySecret 33 + * 34 + * OK 35 + */ 36 + 200: PaginatedResponse & { 37 + data?: Array<RepositorySecret>; 38 + }; 39 + }; 40 + 41 + export type ListSecretsResponse = ListSecretsResponses[keyof ListSecretsResponses]; 42 + 43 + export type CreateSecretData = { 44 + body?: never; 45 + path?: never; 46 + query?: never; 47 + url: '/secrets/{id}'; 48 + }; 49 + 50 + export type CreateSecretResponses = { 51 + /** 52 + * Created 53 + */ 54 + 201: RepositorySecret; 55 + }; 56 + 57 + export type CreateSecretResponse = CreateSecretResponses[keyof CreateSecretResponses];
+1 -1
packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts
··· 283 283 const { response } = operationResponsesMap(operation); 284 284 if (!response) return; 285 285 286 - if (response.items && response.items.length > 1) { 286 + if (response.items && response.items.length > 1 && response.logicalOperator !== 'and') { 287 287 if (plugin.context.config.logs.level === 'debug') { 288 288 console.warn( 289 289 `❗️ Transformers warning: route ${createOperationKey(operation)} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`,
+115
specs/3.0.x/transformers-allof-response-wrapper.json
··· 1 + { 2 + "openapi": "3.0.0", 3 + "info": { 4 + "title": "Test Paginated Response", 5 + "version": "1.0.0" 6 + }, 7 + "components": { 8 + "schemas": { 9 + "PaginatedResponse": { 10 + "type": "object", 11 + "properties": { 12 + "meta": { 13 + "type": "object", 14 + "properties": { 15 + "page": { 16 + "type": "number" 17 + }, 18 + "limit": { 19 + "type": "number" 20 + }, 21 + "total": { 22 + "type": "number" 23 + }, 24 + "totalPages": { 25 + "type": "number" 26 + } 27 + } 28 + } 29 + } 30 + }, 31 + "RepositorySecret": { 32 + "type": "object", 33 + "properties": { 34 + "id": { 35 + "type": "number" 36 + }, 37 + "name": { 38 + "type": "string" 39 + }, 40 + "createdAt": { 41 + "format": "date-time", 42 + "type": "string" 43 + }, 44 + "updatedAt": { 45 + "format": "date-time", 46 + "type": "string" 47 + } 48 + }, 49 + "required": ["id", "name", "createdAt", "updatedAt"] 50 + } 51 + } 52 + }, 53 + "paths": { 54 + "/secrets": { 55 + "get": { 56 + "operationId": "listSecrets", 57 + "responses": { 58 + "200": { 59 + "description": "OK", 60 + "content": { 61 + "application/json": { 62 + "schema": { 63 + "title": "PaginatedResponseRepositorySecret", 64 + "allOf": [ 65 + { 66 + "$ref": "#/components/schemas/PaginatedResponse" 67 + }, 68 + { 69 + "properties": { 70 + "data": { 71 + "type": "array", 72 + "items": { 73 + "$ref": "#/components/schemas/RepositorySecret" 74 + } 75 + } 76 + } 77 + } 78 + ] 79 + } 80 + } 81 + } 82 + } 83 + } 84 + } 85 + }, 86 + "/secrets/{id}": { 87 + "post": { 88 + "operationId": "createSecret", 89 + "responses": { 90 + "201": { 91 + "description": "Created", 92 + "content": { 93 + "application/json": { 94 + "schema": { 95 + "$ref": "#/components/schemas/RepositorySecret" 96 + } 97 + } 98 + } 99 + } 100 + } 101 + } 102 + } 103 + }, 104 + "servers": [ 105 + { 106 + "url": "{protocol}://specs", 107 + "variables": { 108 + "protocol": { 109 + "default": "https", 110 + "enum": ["http", "https"] 111 + } 112 + } 113 + } 114 + ] 115 + }
+91
specs/3.1.x/transformers-allof-response-wrapper.json
··· 1 + { 2 + "openapi": "3.1.0", 3 + "info": { 4 + "title": "Test Paginated Response", 5 + "version": "1.0.0" 6 + }, 7 + "servers": [ 8 + { 9 + "url": "{protocol}://specs", 10 + "variables": { 11 + "protocol": { 12 + "default": "https", 13 + "enum": ["http", "https"] 14 + } 15 + } 16 + } 17 + ], 18 + "components": { 19 + "schemas": { 20 + "PaginatedResponse": { 21 + "type": "object", 22 + "properties": { 23 + "meta": { 24 + "type": "object", 25 + "properties": { 26 + "page": { "type": "number" }, 27 + "limit": { "type": "number" }, 28 + "total": { "type": "number" }, 29 + "totalPages": { "type": "number" } 30 + } 31 + } 32 + } 33 + }, 34 + "RepositorySecret": { 35 + "type": "object", 36 + "properties": { 37 + "id": { "type": "number" }, 38 + "name": { "type": "string" }, 39 + "createdAt": { "format": "date-time", "type": "string" }, 40 + "updatedAt": { "format": "date-time", "type": "string" } 41 + }, 42 + "required": ["id", "name", "createdAt", "updatedAt"] 43 + } 44 + } 45 + }, 46 + "paths": { 47 + "/secrets": { 48 + "get": { 49 + "operationId": "listSecrets", 50 + "responses": { 51 + "200": { 52 + "description": "OK", 53 + "content": { 54 + "application/json": { 55 + "schema": { 56 + "title": "PaginatedResponseRepositorySecret", 57 + "allOf": [ 58 + { "$ref": "#/components/schemas/PaginatedResponse" }, 59 + { 60 + "properties": { 61 + "data": { 62 + "type": "array", 63 + "items": { "$ref": "#/components/schemas/RepositorySecret" } 64 + } 65 + } 66 + } 67 + ] 68 + } 69 + } 70 + } 71 + } 72 + } 73 + } 74 + }, 75 + "/secrets/{id}": { 76 + "post": { 77 + "operationId": "createSecret", 78 + "responses": { 79 + "201": { 80 + "description": "Created", 81 + "content": { 82 + "application/json": { 83 + "schema": { "$ref": "#/components/schemas/RepositorySecret" } 84 + } 85 + } 86 + } 87 + } 88 + } 89 + } 90 + } 91 + }