From a1807fe4ba8256d435a4b5904935b7cc29734dad Mon Sep 17 00:00:00 2001 From: Dmitriy Brolnickij Date: Sun, 14 Sep 2025 17:04:19 +0700 Subject: [PATCH] feat: initialize `ofetch` example project --- examples/openapi-ts-ofetch/env.d.ts | 1 + examples/openapi-ts-ofetch/index.html | 12 + .../openapi-ts-ofetch/openapi-ts.config.ts | 20 + examples/openapi-ts-ofetch/package.json | 45 ++ examples/openapi-ts-ofetch/postcss.config.js | 6 + examples/openapi-ts-ofetch/src/App.vue | 190 +++++ .../openapi-ts-ofetch/src/assets/main.css | 7 + .../src/client/client.gen.ts | 28 + .../src/client/client/client.gen.ts | 236 ++++++ .../src/client/client/index.ts | 25 + .../src/client/client/types.gen.ts | 300 ++++++++ .../src/client/client/utils.gen.ts | 532 +++++++++++++ .../src/client/core/auth.gen.ts | 42 ++ .../src/client/core/bodySerializer.gen.ts | 92 +++ .../src/client/core/params.gen.ts | 153 ++++ .../src/client/core/pathSerializer.gen.ts | 181 +++++ .../src/client/core/serverSentEvents.gen.ts | 264 +++++++ .../src/client/core/types.gen.ts | 118 +++ .../src/client/core/utils.gen.ts | 143 ++++ .../openapi-ts-ofetch/src/client/index.ts | 4 + .../src/client/schemas.gen.ts | 188 +++++ .../openapi-ts-ofetch/src/client/sdk.gen.ts | 467 ++++++++++++ .../openapi-ts-ofetch/src/client/types.gen.ts | 699 ++++++++++++++++++ examples/openapi-ts-ofetch/src/main.ts | 18 + examples/openapi-ts-ofetch/tailwind.config.ts | 9 + examples/openapi-ts-ofetch/tsconfig.app.json | 14 + examples/openapi-ts-ofetch/tsconfig.json | 11 + examples/openapi-ts-ofetch/tsconfig.node.json | 19 + examples/openapi-ts-ofetch/vite.config.ts | 14 + 29 files changed, 3838 insertions(+) create mode 100644 examples/openapi-ts-ofetch/env.d.ts create mode 100644 examples/openapi-ts-ofetch/index.html create mode 100644 examples/openapi-ts-ofetch/openapi-ts.config.ts create mode 100644 examples/openapi-ts-ofetch/package.json create mode 100644 examples/openapi-ts-ofetch/postcss.config.js create mode 100644 examples/openapi-ts-ofetch/src/App.vue create mode 100644 examples/openapi-ts-ofetch/src/assets/main.css create mode 100644 examples/openapi-ts-ofetch/src/client/client.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/client/client.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/client/index.ts create mode 100644 examples/openapi-ts-ofetch/src/client/client/types.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/client/utils.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/auth.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/params.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/pathSerializer.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/types.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/index.ts create mode 100644 examples/openapi-ts-ofetch/src/client/schemas.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/sdk.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/types.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/main.ts create mode 100644 examples/openapi-ts-ofetch/tailwind.config.ts create mode 100644 examples/openapi-ts-ofetch/tsconfig.app.json create mode 100644 examples/openapi-ts-ofetch/tsconfig.json create mode 100644 examples/openapi-ts-ofetch/tsconfig.node.json create mode 100644 examples/openapi-ts-ofetch/vite.config.ts diff --git a/examples/openapi-ts-ofetch/env.d.ts b/examples/openapi-ts-ofetch/env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/examples/openapi-ts-ofetch/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/openapi-ts-ofetch/index.html b/examples/openapi-ts-ofetch/index.html new file mode 100644 index 000000000..c90490343 --- /dev/null +++ b/examples/openapi-ts-ofetch/index.html @@ -0,0 +1,12 @@ + + + + + + Hey API — ofetch example + + +
+ + + diff --git a/examples/openapi-ts-ofetch/openapi-ts.config.ts b/examples/openapi-ts-ofetch/openapi-ts.config.ts new file mode 100644 index 000000000..e0d23642c --- /dev/null +++ b/examples/openapi-ts-ofetch/openapi-ts.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@hey-api/openapi-ts'; + +export default defineConfig({ + input: + 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', + output: { + format: 'prettier', + lint: 'eslint', + path: './src/client', + }, + plugins: [ + '@hey-api/client-ofetch', + '@hey-api/schemas', + '@hey-api/sdk', + { + enums: 'javascript', + name: '@hey-api/typescript', + }, + ], +}); diff --git a/examples/openapi-ts-ofetch/package.json b/examples/openapi-ts-ofetch/package.json new file mode 100644 index 000000000..6427931e6 --- /dev/null +++ b/examples/openapi-ts-ofetch/package.json @@ -0,0 +1,45 @@ +{ + "name": "@example/openapi-ts-ofetch", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix --ignore-path .gitignore", + "openapi-ts": "openapi-ts", + "preview": "vite preview", + "typecheck": "vue-tsc --build --force" + }, + "dependencies": { + "ofetch": "1.4.1", + "vue": "3.5.21" + }, + "devDependencies": { + "@config/vite-base": "workspace:*", + "@hey-api/openapi-ts": "workspace:*", + "@rushstack/eslint-patch": "1.10.5", + "@tsconfig/node20": "20.1.4", + "@types/jsdom": "21.1.7", + "@types/node": "22.10.5", + "@vitejs/plugin-vue": "5.2.1", + "@vitejs/plugin-vue-jsx": "4.1.1", + "@vue/eslint-config-prettier": "10.1.0", + "@vue/eslint-config-typescript": "14.2.0", + "@vue/test-utils": "2.4.6", + "@vue/tsconfig": "0.7.0", + "autoprefixer": "10.4.20", + "eslint": "9.17.0", + "eslint-plugin-vue": "9.32.0", + "jsdom": "23.0.0", + "npm-run-all2": "6.2.0", + "postcss": "8.4.41", + "prettier": "3.4.2", + "tailwindcss": "3.4.9", + "typescript": "5.8.3", + "vite": "7.1.2", + "vite-plugin-vue-devtools": "7.7.0", + "vitest": "3.1.1", + "vue-tsc": "2.2.0" + } +} diff --git a/examples/openapi-ts-ofetch/postcss.config.js b/examples/openapi-ts-ofetch/postcss.config.js new file mode 100644 index 000000000..9eef821c4 --- /dev/null +++ b/examples/openapi-ts-ofetch/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + autoprefixer: {}, + tailwindcss: {}, + }, +}; diff --git a/examples/openapi-ts-ofetch/src/App.vue b/examples/openapi-ts-ofetch/src/App.vue new file mode 100644 index 000000000..d48b4c201 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/App.vue @@ -0,0 +1,190 @@ + + + diff --git a/examples/openapi-ts-ofetch/src/assets/main.css b/examples/openapi-ts-ofetch/src/assets/main.css new file mode 100644 index 000000000..0a2ac6aac --- /dev/null +++ b/examples/openapi-ts-ofetch/src/assets/main.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-[#111113]; +} diff --git a/examples/openapi-ts-ofetch/src/client/client.gen.ts b/examples/openapi-ts-ofetch/src/client/client.gen.ts new file mode 100644 index 000000000..f1e680045 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/client.gen.ts @@ -0,0 +1,28 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type ClientOptions as DefaultClientOptions, + type Config, + createClient, + createConfig, +} from './client'; +import type { ClientOptions } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = + ( + override?: Config, + ) => Config & T>; + +export const client = createClient( + createConfig({ + baseUrl: 'https://petstore3.swagger.io/api/v3', + }), +); diff --git a/examples/openapi-ts-ofetch/src/client/client/client.gen.ts b/examples/openapi-ts-ofetch/src/client/client/client.gen.ts new file mode 100644 index 000000000..d2482df58 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/client/client.gen.ts @@ -0,0 +1,236 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { ofetch, type ResponseType as OfetchResponseType } from 'ofetch'; + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildOfetchOptions, + buildUrl, + createConfig, + createInterceptors, + isRepeatableBody, + mapParseAsToResponseType, + mergeConfigs, + mergeHeaders, + parseError, + parseSuccess, + setAuthParams, + wrapDataReturn, + wrapErrorReturn, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + // Precompute network body for retries and consistent handling + const networkBody = getValidRequestBody(opts) as + | RequestInit['body'] + | null + | undefined; + + const url = buildUrl(opts); + + return { networkBody, opts, url }; + }; + + const request: Client['request'] = async (options) => { + const { + networkBody: initialNetworkBody, + opts, + url, + } = await beforeRequest(options as any); + // Compute response type mapping once + const ofetchResponseType: OfetchResponseType | undefined = + mapParseAsToResponseType(opts.parseAs, opts.responseType); + + const $ofetch = opts.ofetch ?? ofetch; + + // Always create Request pre-network (align with client-fetch) + let networkBody = initialNetworkBody; + const requestInit: ReqInit = { + body: networkBody, + headers: opts.headers as Headers, + method: opts.method, + redirect: 'follow', + signal: opts.signal, + }; + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // Reflect any interceptor changes into opts used for network and downstream + opts.headers = request.headers; + opts.method = request.method as Uppercase; + // Attempt to reflect possible signal/body changes (safely) + + const reqBody = (request as any).body as unknown; + let effectiveRetry = opts.retry; + if (reqBody !== undefined && reqBody !== null) { + if (isRepeatableBody(reqBody)) { + networkBody = reqBody as BodyInit; + } else { + networkBody = reqBody as BodyInit; + // Disable retries for non-repeatable bodies + effectiveRetry = 0 as any; + } + } + + opts.signal = (request as any).signal as AbortSignal | undefined; + const finalUrl = request.url; + + // Build ofetch options and perform the request + const responseOptions = buildOfetchOptions( + opts as ResolvedRequestOptions, + networkBody ?? undefined, + effectiveRetry as any, + ); + + let response = await $ofetch.raw(finalUrl, responseOptions); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { request, response }; + + if (response.ok) { + const data = await parseSuccess(response, opts, ofetchResponseType); + return wrapDataReturn(data, result, opts.responseStyle); + } + + let finalError = await parseError(response); + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = await fn(finalError, response, request, opts); + } + } + + // Ensure error is never undefined after interceptors + finalError = (finalError as any) || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + return wrapErrorReturn(finalError, result, opts.responseStyle) as any; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method } as any); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { networkBody, opts, url } = await beforeRequest(options); + const optsForSse: any = { ...opts }; + delete optsForSse.body; + return createSseClient({ + ...optsForSse, + fetch: opts.fetch, + headers: opts.headers as Headers, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: networkBody as BodyInit | null | undefined, + signal: opts.signal, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-ofetch/src/client/client/index.ts b/examples/openapi-ts-ofetch/src/client/client/index.ts new file mode 100644 index 000000000..318a84b6a --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-ofetch/src/client/client/types.gen.ts b/examples/openapi-ts-ofetch/src/client/client/types.gen.ts new file mode 100644 index 000000000..e4925b81b --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/client/types.gen.ts @@ -0,0 +1,300 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + FetchOptions as OfetchOptions, + ResponseType as OfetchResponseType, +} from 'ofetch'; +import type { ofetch } from 'ofetch'; + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + agent?: OfetchOptions['agent']; + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** Node-only proxy/agent options */ + dispatcher?: OfetchOptions['dispatcher']; + /** Optional fetch instance used for SSE streaming */ + fetch?: typeof fetch; + // No custom fetch option: provide custom instance via `ofetch` instead + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Custom ofetch instance created via `ofetch.create()`. If provided, it will + * be used for requests instead of the default `ofetch` export. + */ + ofetch?: typeof ofetch; + /** ofetch interceptors and runtime options */ + onRequest?: OfetchOptions['onRequest']; + onRequestError?: OfetchOptions['onRequestError']; + onResponse?: OfetchOptions['onResponse']; + onResponseError?: OfetchOptions['onResponseError']; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** Custom response parser (ofetch). */ + parseResponse?: OfetchOptions['parseResponse']; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * ofetch responseType override. If provided, it will be passed directly to + * ofetch and take precedence over `parseAs`. + */ + responseType?: OfetchResponseType; + /** + * Automatically retry failed requests. + */ + retry?: OfetchOptions['retry']; + retryDelay?: OfetchOptions['retryDelay']; + retryStatusCodes?: OfetchOptions['retryStatusCodes']; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; + /** + * Abort the request after the given milliseconds. + */ + timeout?: number; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts b/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts new file mode 100644 index 000000000..17c601b39 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/client/utils.gen.ts @@ -0,0 +1,532 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + FetchOptions as OfetchOptions, + ResponseType as OfetchResponseType, +} from 'ofetch'; + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, + ResolvedRequestOptions, + ResponseStyle, +} from './types.gen'; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +/** + * Map our parseAs value to ofetch responseType when not explicitly provided. + */ +export const mapParseAsToResponseType = ( + parseAs: Config['parseAs'] | undefined, + explicit?: OfetchResponseType, +): OfetchResponseType | undefined => { + if (explicit) return explicit; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'json': + case 'text': + case 'stream': + return parseAs; + case 'formData': + case 'auto': + default: + return undefined; // let ofetch auto-detect + } +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +/** + * Heuristic to detect whether a request body can be safely retried. + */ +export const isRepeatableBody = (body: unknown): boolean => { + if (body == null) return true; // undefined/null treated as no-body + if (typeof body === 'string') return true; + if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) + return true; + if (typeof Uint8Array !== 'undefined' && body instanceof Uint8Array) + return true; + if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) + return true; + if (typeof Blob !== 'undefined' && body instanceof Blob) return true; + if (typeof FormData !== 'undefined' && body instanceof FormData) return true; + // Streams are not repeatable + if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) + return false; + // Default: assume non-repeatable for unknown structured bodies + return false; +}; + +/** + * Small helper to unify data vs fields return style. + */ +export const wrapDataReturn = ( + data: T, + result: { request: Request; response: Response }, + responseStyle: ResponseStyle | undefined, +): + | T + | ((T extends Record ? { data: T } : { data: T }) & + typeof result) => + (responseStyle ?? 'fields') === 'data' + ? (data as any) + : ({ data, ...result } as any); + +/** + * Small helper to unify error vs fields return style. + */ +export const wrapErrorReturn = ( + error: E, + result: { request: Request; response: Response }, + responseStyle: ResponseStyle | undefined, +): + | undefined + | ((E extends Record ? { error: E } : { error: E }) & + typeof result) => + (responseStyle ?? 'fields') === 'data' + ? undefined + : ({ error, ...result } as any); + +/** + * Build options for $ofetch.raw from our resolved opts and body. + */ +export const buildOfetchOptions = ( + opts: ResolvedRequestOptions, + body: BodyInit | null | undefined, + retryOverride?: OfetchOptions['retry'], +): OfetchOptions => { + const responseType = mapParseAsToResponseType( + opts.parseAs, + opts.responseType, + ); + return { + agent: opts.agent as OfetchOptions['agent'], + body: body as any, + dispatcher: opts.dispatcher as OfetchOptions['dispatcher'], + headers: opts.headers as Headers, + method: opts.method, + onRequest: opts.onRequest as OfetchOptions['onRequest'], + onRequestError: opts.onRequestError as OfetchOptions['onRequestError'], + onResponse: opts.onResponse as OfetchOptions['onResponse'], + onResponseError: opts.onResponseError as OfetchOptions['onResponseError'], + parseResponse: opts.parseResponse as OfetchOptions['parseResponse'], + query: undefined, // URL already includes query + responseType, + retry: (retryOverride ?? + (opts.retry as OfetchOptions['retry'])) as OfetchOptions['retry'], + retryDelay: opts.retryDelay as OfetchOptions['retryDelay'], + retryStatusCodes: + opts.retryStatusCodes as OfetchOptions['retryStatusCodes'], + signal: opts.signal, + timeout: opts.timeout as number | undefined, + } as OfetchOptions; +}; + +/** + * Parse a successful response, handling empty bodies and stream cases. + */ +export const parseSuccess = async ( + response: Response, + opts: ResolvedRequestOptions, + ofetchResponseType?: OfetchResponseType, +): Promise => { + // Stream requested: return stream body + if (ofetchResponseType === 'stream') { + return response.body; + } + + const inferredParseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + // Handle empty responses + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + switch (inferredParseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + return await (response as any)[inferredParseAs](); + case 'formData': + return new FormData(); + case 'stream': + return response.body; + default: + return {}; + } + } + + // Prefer ofetch-populated data + let data: unknown = (response as any)._data; + if (typeof data === 'undefined') { + switch (inferredParseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await (response as any)[inferredParseAs](); + break; + case 'stream': + return response.body; + } + } + + if (inferredParseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return data; +}; + +/** + * Parse an error response payload. + */ +export const parseError = async (response: Response): Promise => { + let error: unknown = (response as any)._data; + if (typeof error === 'undefined') { + const textError = await response.text(); + try { + error = JSON.parse(textError); + } catch { + error = textError; + } + } + return error ?? ({} as string); +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/examples/openapi-ts-ofetch/src/client/core/auth.gen.ts b/examples/openapi-ts-ofetch/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..49cd8925e --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/params.gen.ts b/examples/openapi-ts-ofetch/src/client/core/params.gen.ts new file mode 100644 index 000000000..71c88e852 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-ofetch/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/types.gen.ts b/examples/openapi-ts-ofetch/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-ofetch/src/client/core/utils.gen.ts b/examples/openapi-ts-ofetch/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-ofetch/src/client/index.ts b/examples/openapi-ts-ofetch/src/client/index.ts new file mode 100644 index 000000000..f796d2cc8 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/index.ts @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export * from './sdk.gen'; +export * from './types.gen'; diff --git a/examples/openapi-ts-ofetch/src/client/schemas.gen.ts b/examples/openapi-ts-ofetch/src/client/schemas.gen.ts new file mode 100644 index 000000000..646632e83 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/schemas.gen.ts @@ -0,0 +1,188 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const OrderSchema = { + properties: { + complete: { + type: 'boolean', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + petId: { + example: 198772, + format: 'int64', + type: 'integer', + }, + quantity: { + example: 7, + format: 'int32', + type: 'integer', + }, + shipDate: { + format: 'date-time', + type: 'string', + }, + status: { + description: 'Order Status', + enum: ['placed', 'approved', 'delivered'], + example: 'approved', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Order', + xml: { + name: 'order', + }, +} as const; + +export const CategorySchema = { + properties: { + id: { + example: 1, + format: 'int64', + type: 'integer', + }, + name: { + example: 'Dogs', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Category', + xml: { + name: 'category', + }, +} as const; + +export const UserSchema = { + properties: { + email: { + example: 'john@email.com', + type: 'string', + }, + firstName: { + example: 'John', + type: 'string', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + lastName: { + example: 'James', + type: 'string', + }, + password: { + example: '12345', + type: 'string', + }, + phone: { + example: '12345', + type: 'string', + }, + userStatus: { + description: 'User Status', + example: 1, + format: 'int32', + type: 'integer', + }, + username: { + example: 'theUser', + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.User', + xml: { + name: 'user', + }, +} as const; + +export const TagSchema = { + properties: { + id: { + format: 'int64', + type: 'integer', + }, + name: { + type: 'string', + }, + }, + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Tag', + xml: { + name: 'tag', + }, +} as const; + +export const PetSchema = { + properties: { + category: { + $ref: '#/components/schemas/Category', + }, + id: { + example: 10, + format: 'int64', + type: 'integer', + }, + name: { + example: 'doggie', + type: 'string', + }, + photoUrls: { + items: { + type: 'string', + xml: { + name: 'photoUrl', + }, + }, + type: 'array', + xml: { + wrapped: true, + }, + }, + status: { + description: 'pet status in the store', + enum: ['available', 'pending', 'sold'], + type: 'string', + }, + tags: { + items: { + $ref: '#/components/schemas/Tag', + }, + type: 'array', + xml: { + wrapped: true, + }, + }, + }, + required: ['name', 'photoUrls'], + type: 'object', + 'x-swagger-router-model': 'io.swagger.petstore.model.Pet', + xml: { + name: 'pet', + }, +} as const; + +export const ApiResponseSchema = { + properties: { + code: { + format: 'int32', + type: 'integer', + }, + message: { + type: 'string', + }, + type: { + type: 'string', + }, + }, + type: 'object', + xml: { + name: '##default', + }, +} as const; diff --git a/examples/openapi-ts-ofetch/src/client/sdk.gen.ts b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts new file mode 100644 index 000000000..214fbf715 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts @@ -0,0 +1,467 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as ClientOptions, TDataShape } from './client'; +import { client } from './client.gen'; +import type { + AddPetData, + AddPetErrors, + AddPetResponses, + CreateUserData, + CreateUserErrors, + CreateUserResponses, + CreateUsersWithListInputData, + CreateUsersWithListInputErrors, + CreateUsersWithListInputResponses, + DeleteOrderData, + DeleteOrderErrors, + DeleteOrderResponses, + DeletePetData, + DeletePetErrors, + DeletePetResponses, + DeleteUserData, + DeleteUserErrors, + DeleteUserResponses, + FindPetsByStatusData, + FindPetsByStatusErrors, + FindPetsByStatusResponses, + FindPetsByTagsData, + FindPetsByTagsErrors, + FindPetsByTagsResponses, + GetInventoryData, + GetInventoryErrors, + GetInventoryResponses, + GetOrderByIdData, + GetOrderByIdErrors, + GetOrderByIdResponses, + GetPetByIdData, + GetPetByIdErrors, + GetPetByIdResponses, + GetUserByNameData, + GetUserByNameErrors, + GetUserByNameResponses, + LoginUserData, + LoginUserErrors, + LoginUserResponses, + LogoutUserData, + LogoutUserErrors, + LogoutUserResponses, + PlaceOrderData, + PlaceOrderErrors, + PlaceOrderResponses, + UpdatePetData, + UpdatePetErrors, + UpdatePetResponses, + UpdatePetWithFormData, + UpdatePetWithFormErrors, + UpdatePetWithFormResponses, + UpdateUserData, + UpdateUserErrors, + UpdateUserResponses, + UploadFileData, + UploadFileErrors, + UploadFileResponses, +} from './types.gen'; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, +> = ClientOptions & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Add a new pet to the store. + * Add a new pet to the store. + */ +export const addPet = ( + options: Options, +) => + (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * Update an existing pet. + * Update an existing pet by Id. + */ +export const updatePet = ( + options: Options, +) => + (options.client ?? client).put< + UpdatePetResponses, + UpdatePetErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); + +/** + * Finds Pets by status. + * Multiple status values can be provided with comma separated strings. + */ +export const findPetsByStatus = ( + options: Options, +) => + (options.client ?? client).get< + FindPetsByStatusResponses, + FindPetsByStatusErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/findByStatus', + ...options, + }); + +/** + * Finds Pets by tags. + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + */ +export const findPetsByTags = ( + options: Options, +) => + (options.client ?? client).get< + FindPetsByTagsResponses, + FindPetsByTagsErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/findByTags', + ...options, + }); + +/** + * Deletes a pet. + * Delete a pet. + */ +export const deletePet = ( + options: Options, +) => + (options.client ?? client).delete< + DeletePetResponses, + DeletePetErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Find pet by ID. + * Returns a single pet. + */ +export const getPetById = ( + options: Options, +) => + (options.client ?? client).get< + GetPetByIdResponses, + GetPetByIdErrors, + ThrowOnError + >({ + security: [ + { + name: 'api_key', + type: 'apiKey', + }, + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Updates a pet in the store with form data. + * Updates a pet resource based on the form data. + */ +export const updatePetWithForm = ( + options: Options, +) => + (options.client ?? client).post< + UpdatePetWithFormResponses, + UpdatePetWithFormErrors, + ThrowOnError + >({ + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}', + ...options, + }); + +/** + * Uploads an image. + * Upload image of the pet. + */ +export const uploadFile = ( + options: Options, +) => + (options.client ?? client).post< + UploadFileResponses, + UploadFileErrors, + ThrowOnError + >({ + bodySerializer: null, + security: [ + { + scheme: 'bearer', + type: 'http', + }, + ], + url: '/pet/{petId}/uploadImage', + ...options, + headers: { + 'Content-Type': 'application/octet-stream', + ...options.headers, + }, + }); + +/** + * Returns pet inventories by status. + * Returns a map of status codes to quantities. + */ +export const getInventory = ( + options?: Options, +) => + (options?.client ?? client).get< + GetInventoryResponses, + GetInventoryErrors, + ThrowOnError + >({ + security: [ + { + name: 'api_key', + type: 'apiKey', + }, + ], + url: '/store/inventory', + ...options, + }); + +/** + * Place an order for a pet. + * Place a new order in the store. + */ +export const placeOrder = ( + options?: Options, +) => + (options?.client ?? client).post< + PlaceOrderResponses, + PlaceOrderErrors, + ThrowOnError + >({ + url: '/store/order', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Delete purchase order by identifier. + * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. + */ +export const deleteOrder = ( + options: Options, +) => + (options.client ?? client).delete< + DeleteOrderResponses, + DeleteOrderErrors, + ThrowOnError + >({ + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Find purchase order by ID. + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + */ +export const getOrderById = ( + options: Options, +) => + (options.client ?? client).get< + GetOrderByIdResponses, + GetOrderByIdErrors, + ThrowOnError + >({ + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Create user. + * This can only be done by the logged in user. + */ +export const createUser = ( + options?: Options, +) => + (options?.client ?? client).post< + CreateUserResponses, + CreateUserErrors, + ThrowOnError + >({ + url: '/user', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Creates list of users with given input array. + * Creates list of users with given input array. + */ +export const createUsersWithListInput = ( + options?: Options, +) => + (options?.client ?? client).post< + CreateUsersWithListInputResponses, + CreateUsersWithListInputErrors, + ThrowOnError + >({ + url: '/user/createWithList', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + +/** + * Logs user into the system. + * Log into the system. + */ +export const loginUser = ( + options?: Options, +) => + (options?.client ?? client).get< + LoginUserResponses, + LoginUserErrors, + ThrowOnError + >({ + url: '/user/login', + ...options, + }); + +/** + * Logs out current logged in user session. + * Log user out of the system. + */ +export const logoutUser = ( + options?: Options, +) => + (options?.client ?? client).get< + LogoutUserResponses, + LogoutUserErrors, + ThrowOnError + >({ + url: '/user/logout', + ...options, + }); + +/** + * Delete user resource. + * This can only be done by the logged in user. + */ +export const deleteUser = ( + options: Options, +) => + (options.client ?? client).delete< + DeleteUserResponses, + DeleteUserErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + }); + +/** + * Get user by user name. + * Get user detail based on username. + */ +export const getUserByName = ( + options: Options, +) => + (options.client ?? client).get< + GetUserByNameResponses, + GetUserByNameErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + }); + +/** + * Update user resource. + * This can only be done by the logged in user. + */ +export const updateUser = ( + options: Options, +) => + (options.client ?? client).put< + UpdateUserResponses, + UpdateUserErrors, + ThrowOnError + >({ + url: '/user/{username}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }); diff --git a/examples/openapi-ts-ofetch/src/client/types.gen.ts b/examples/openapi-ts-ofetch/src/client/types.gen.ts new file mode 100644 index 000000000..992c17fb2 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/types.gen.ts @@ -0,0 +1,699 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type Order = { + complete?: boolean; + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** + * Order Status + */ + status?: 'placed' | 'approved' | 'delivered'; +}; + +export type Category = { + id?: number; + name?: string; +}; + +export type User = { + email?: string; + firstName?: string; + id?: number; + lastName?: string; + password?: string; + phone?: string; + /** + * User Status + */ + userStatus?: number; + username?: string; +}; + +export type Tag = { + id?: number; + name?: string; +}; + +export type Pet = { + category?: Category; + id?: number; + name: string; + photoUrls: Array; + /** + * pet status in the store + */ + status?: 'available' | 'pending' | 'sold'; + tags?: Array; +}; + +export type ApiResponse = { + code?: number; + message?: string; + type?: string; +}; + +export type Pet2 = Pet; + +/** + * List of user object + */ +export type UserArray = Array; + +export type AddPetData = { + /** + * Create a new pet in the store + */ + body: Pet; + path?: never; + query?: never; + url: '/pet'; +}; + +export type AddPetErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type AddPetResponses = { + /** + * Successful operation + */ + 200: Pet; +}; + +export type AddPetResponse = AddPetResponses[keyof AddPetResponses]; + +export type UpdatePetData = { + /** + * Update an existent pet in the store + */ + body: Pet; + path?: never; + query?: never; + url: '/pet'; +}; + +export type UpdatePetErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdatePetResponses = { + /** + * Successful operation + */ + 200: Pet; +}; + +export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; + +export type FindPetsByStatusData = { + body?: never; + path?: never; + query: { + /** + * Status values that need to be considered for filter + */ + status: 'available' | 'pending' | 'sold'; + }; + url: '/pet/findByStatus'; +}; + +export type FindPetsByStatusErrors = { + /** + * Invalid status value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type FindPetsByStatusResponses = { + /** + * successful operation + */ + 200: Array; +}; + +export type FindPetsByStatusResponse = + FindPetsByStatusResponses[keyof FindPetsByStatusResponses]; + +export type FindPetsByTagsData = { + body?: never; + path?: never; + query: { + /** + * Tags to filter by + */ + tags: Array; + }; + url: '/pet/findByTags'; +}; + +export type FindPetsByTagsErrors = { + /** + * Invalid tag value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type FindPetsByTagsResponses = { + /** + * successful operation + */ + 200: Array; +}; + +export type FindPetsByTagsResponse = + FindPetsByTagsResponses[keyof FindPetsByTagsResponses]; + +export type DeletePetData = { + body?: never; + headers?: { + api_key?: string; + }; + path: { + /** + * Pet id to delete + */ + petId: number; + }; + query?: never; + url: '/pet/{petId}'; +}; + +export type DeletePetErrors = { + /** + * Invalid pet value + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeletePetResponses = { + /** + * Pet deleted + */ + 200: unknown; +}; + +export type GetPetByIdData = { + body?: never; + path: { + /** + * ID of pet to return + */ + petId: number; + }; + query?: never; + url: '/pet/{petId}'; +}; + +export type GetPetByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetPetByIdResponses = { + /** + * successful operation + */ + 200: Pet; +}; + +export type GetPetByIdResponse = GetPetByIdResponses[keyof GetPetByIdResponses]; + +export type UpdatePetWithFormData = { + body?: never; + path: { + /** + * ID of pet that needs to be updated + */ + petId: number; + }; + query?: { + /** + * Name of pet that needs to be updated + */ + name?: string; + /** + * Status of pet that needs to be updated + */ + status?: string; + }; + url: '/pet/{petId}'; +}; + +export type UpdatePetWithFormErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdatePetWithFormResponses = { + /** + * successful operation + */ + 200: Pet; +}; + +export type UpdatePetWithFormResponse = + UpdatePetWithFormResponses[keyof UpdatePetWithFormResponses]; + +export type UploadFileData = { + body?: Blob | File; + path: { + /** + * ID of pet to update + */ + petId: number; + }; + query?: { + /** + * Additional Metadata + */ + additionalMetadata?: string; + }; + url: '/pet/{petId}/uploadImage'; +}; + +export type UploadFileErrors = { + /** + * No file uploaded + */ + 400: unknown; + /** + * Pet not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UploadFileResponses = { + /** + * successful operation + */ + 200: ApiResponse; +}; + +export type UploadFileResponse = UploadFileResponses[keyof UploadFileResponses]; + +export type GetInventoryData = { + body?: never; + path?: never; + query?: never; + url: '/store/inventory'; +}; + +export type GetInventoryErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetInventoryResponses = { + /** + * successful operation + */ + 200: { + [key: string]: number; + }; +}; + +export type GetInventoryResponse = + GetInventoryResponses[keyof GetInventoryResponses]; + +export type PlaceOrderData = { + body?: Order; + path?: never; + query?: never; + url: '/store/order'; +}; + +export type PlaceOrderErrors = { + /** + * Invalid input + */ + 400: unknown; + /** + * Validation exception + */ + 422: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type PlaceOrderResponses = { + /** + * successful operation + */ + 200: Order; +}; + +export type PlaceOrderResponse = PlaceOrderResponses[keyof PlaceOrderResponses]; + +export type DeleteOrderData = { + body?: never; + path: { + /** + * ID of the order that needs to be deleted + */ + orderId: number; + }; + query?: never; + url: '/store/order/{orderId}'; +}; + +export type DeleteOrderErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Order not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeleteOrderResponses = { + /** + * order deleted + */ + 200: unknown; +}; + +export type GetOrderByIdData = { + body?: never; + path: { + /** + * ID of order that needs to be fetched + */ + orderId: number; + }; + query?: never; + url: '/store/order/{orderId}'; +}; + +export type GetOrderByIdErrors = { + /** + * Invalid ID supplied + */ + 400: unknown; + /** + * Order not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetOrderByIdResponses = { + /** + * successful operation + */ + 200: Order; +}; + +export type GetOrderByIdResponse = + GetOrderByIdResponses[keyof GetOrderByIdResponses]; + +export type CreateUserData = { + /** + * Created user object + */ + body?: User; + path?: never; + query?: never; + url: '/user'; +}; + +export type CreateUserErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type CreateUserResponses = { + /** + * successful operation + */ + 200: User; +}; + +export type CreateUserResponse = CreateUserResponses[keyof CreateUserResponses]; + +export type CreateUsersWithListInputData = { + body?: Array; + path?: never; + query?: never; + url: '/user/createWithList'; +}; + +export type CreateUsersWithListInputErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type CreateUsersWithListInputResponses = { + /** + * Successful operation + */ + 200: User; +}; + +export type CreateUsersWithListInputResponse = + CreateUsersWithListInputResponses[keyof CreateUsersWithListInputResponses]; + +export type LoginUserData = { + body?: never; + path?: never; + query?: { + /** + * The password for login in clear text + */ + password?: string; + /** + * The user name for login + */ + username?: string; + }; + url: '/user/login'; +}; + +export type LoginUserErrors = { + /** + * Invalid username/password supplied + */ + 400: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type LoginUserResponses = { + /** + * successful operation + */ + 200: string; +}; + +export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; + +export type LogoutUserData = { + body?: never; + path?: never; + query?: never; + url: '/user/logout'; +}; + +export type LogoutUserErrors = { + /** + * Unexpected error + */ + default: unknown; +}; + +export type LogoutUserResponses = { + /** + * successful operation + */ + 200: unknown; +}; + +export type DeleteUserData = { + body?: never; + path: { + /** + * The name that needs to be deleted + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type DeleteUserErrors = { + /** + * Invalid username supplied + */ + 400: unknown; + /** + * User not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type DeleteUserResponses = { + /** + * User deleted + */ + 200: unknown; +}; + +export type GetUserByNameData = { + body?: never; + path: { + /** + * The name that needs to be fetched. Use user1 for testing + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type GetUserByNameErrors = { + /** + * Invalid username supplied + */ + 400: unknown; + /** + * User not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type GetUserByNameResponses = { + /** + * successful operation + */ + 200: User; +}; + +export type GetUserByNameResponse = + GetUserByNameResponses[keyof GetUserByNameResponses]; + +export type UpdateUserData = { + /** + * Update an existent user in the store + */ + body?: User; + path: { + /** + * name that need to be deleted + */ + username: string; + }; + query?: never; + url: '/user/{username}'; +}; + +export type UpdateUserErrors = { + /** + * bad request + */ + 400: unknown; + /** + * user not found + */ + 404: unknown; + /** + * Unexpected error + */ + default: unknown; +}; + +export type UpdateUserResponses = { + /** + * successful operation + */ + 200: unknown; +}; + +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; diff --git a/examples/openapi-ts-ofetch/src/main.ts b/examples/openapi-ts-ofetch/src/main.ts new file mode 100644 index 000000000..a4e1dc9de --- /dev/null +++ b/examples/openapi-ts-ofetch/src/main.ts @@ -0,0 +1,18 @@ +import './assets/main.css'; + +import { createApp } from 'vue'; + +import App from './App.vue'; +import { client } from './client/client.gen'; + +// configure internal service client +client.setConfig({ + // set default base url for requests + baseUrl: 'https://petstore3.swagger.io/api/v3', + // set default headers for requests + headers: { + Authorization: 'Bearer ', + }, +}); + +createApp(App).mount('#app'); diff --git a/examples/openapi-ts-ofetch/tailwind.config.ts b/examples/openapi-ts-ofetch/tailwind.config.ts new file mode 100644 index 000000000..4cbe78a03 --- /dev/null +++ b/examples/openapi-ts-ofetch/tailwind.config.ts @@ -0,0 +1,9 @@ +import type { Config } from 'tailwindcss'; + +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + plugins: [], + theme: { + extend: {}, + }, +} satisfies Config; diff --git a/examples/openapi-ts-ofetch/tsconfig.app.json b/examples/openapi-ts-ofetch/tsconfig.app.json new file mode 100644 index 000000000..81efb53e9 --- /dev/null +++ b/examples/openapi-ts-ofetch/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["./env.d.ts", "./src/**/*", "./src/**/*.vue"], + "exclude": ["./src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/examples/openapi-ts-ofetch/tsconfig.json b/examples/openapi-ts-ofetch/tsconfig.json new file mode 100644 index 000000000..66b5e5703 --- /dev/null +++ b/examples/openapi-ts-ofetch/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/examples/openapi-ts-ofetch/tsconfig.node.json b/examples/openapi-ts-ofetch/tsconfig.node.json new file mode 100644 index 000000000..f09406303 --- /dev/null +++ b/examples/openapi-ts-ofetch/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/examples/openapi-ts-ofetch/vite.config.ts b/examples/openapi-ts-ofetch/vite.config.ts new file mode 100644 index 000000000..9f2a06a37 --- /dev/null +++ b/examples/openapi-ts-ofetch/vite.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { createViteConfig } from '@config/vite-base'; +import vue from '@vitejs/plugin-vue'; + +// https://vitejs.dev/config/ +export default createViteConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}); -- 2.51.2