@hey-api/openapi-ts
Version:
🌀 OpenAPI to TypeScript code generator. Generate API clients, SDKs, validators, and more.
340 lines (295 loc) • 8.24 kB
text/typescript
import type { HTTPError, Options as KyOptions } from 'ky';
import ky from 'ky';
import { createSseClient } from '../core/serverSentEvents';
import type { HttpMethod } from '../core/types';
import { getValidRequestBody } from '../core/utils';
import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
RetryOptions,
} from './types';
import type { Middleware } from './utils';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils';
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),
ky: options.ky ?? _config.ky ?? ky,
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);
}
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts);
return { opts, url };
};
const parseErrorResponse = async (
response: Response,
request: Request,
opts: ResolvedRequestOptions,
interceptorsMiddleware: Middleware<
Request,
Response,
unknown,
ResolvedRequestOptions
>,
) => {
const result = {
request,
response,
};
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
jsonError = undefined;
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptorsMiddleware.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const kyInstance = opts.ky!;
const validBody = getValidRequestBody(opts);
const kyOptions: KyOptions = {
body: validBody as BodyInit,
cache: opts.cache,
credentials: opts.credentials,
headers: opts.headers,
integrity: opts.integrity,
keepalive: opts.keepalive,
method: opts.method as KyOptions['method'],
mode: opts.mode,
redirect: 'follow',
referrer: opts.referrer,
referrerPolicy: opts.referrerPolicy,
signal: opts.signal,
throwHttpErrors: opts.throwOnError ?? false,
timeout: opts.timeout,
...opts.kyOptions,
};
if (opts.retry && typeof opts.retry === 'object') {
const retryOpts = opts.retry as RetryOptions;
kyOptions.retry = {
limit: retryOpts.limit ?? 2,
methods: retryOpts.methods as Array<
| 'get'
| 'post'
| 'put'
| 'patch'
| 'head'
| 'delete'
| 'options'
| 'trace'
>,
statusCodes: retryOpts.statusCodes,
};
}
let request = new Request(url, {
body: kyOptions.body as BodyInit,
headers: kyOptions.headers as HeadersInit,
method: kyOptions.method,
});
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
let response: Response;
try {
response = await kyInstance(request, kyOptions);
} catch (error) {
if (error && typeof error === 'object' && 'response' in error) {
const httpError = error as HTTPError;
response = httpError.response;
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
return parseErrorResponse(response, request, opts, interceptors);
}
throw error;
}
for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
let emptyData: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'text':
emptyData = await response[parseAs]();
break;
case 'formData':
emptyData = new FormData();
break;
case 'stream':
emptyData = response.body;
break;
case 'json':
default:
emptyData = {};
break;
}
return opts.responseStyle === 'data'
? emptyData
: {
data: emptyData,
...result,
};
}
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
return parseErrorResponse(response, request, opts, interceptors);
};
const makeMethodFn =
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn =
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
fetch: globalThis.fetch,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
url,
});
};
return {
buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};