Skip to content

Commit 7023a42

Browse files
brolnickijmrlubos
authored andcommitted
feat(ofetch): init core client func & config
1 parent b59afd5 commit 7023a42

File tree

8 files changed

+1200
-0
lines changed

8 files changed

+1200
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ICodegenSymbolSelector } from '@hey-api/codegen-core';
2+
3+
import type { Plugin } from '../../types';
4+
5+
type SelectorType = 'client';
6+
7+
export type IApi = {
8+
/**
9+
* @param type Selector type.
10+
* @param value Depends on `type`:
11+
* - `client`: never
12+
* @returns Selector array
13+
*/
14+
getSelector: (type: SelectorType, value?: string) => ICodegenSymbolSelector;
15+
};
16+
17+
export class Api implements IApi {
18+
constructor(public meta: Plugin.Name<'@hey-api/client-ofetch'>) {}
19+
20+
getSelector(
21+
...args: ReadonlyArray<string | undefined>
22+
): ICodegenSymbolSelector {
23+
return [this.meta.name, ...(args as ICodegenSymbolSelector)];
24+
}
25+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { ofetch, type ResponseType as OfetchResponseType } from 'ofetch';
2+
3+
import { createSseClient } from '../../client-core/bundle/serverSentEvents';
4+
import type { HttpMethod } from '../../client-core/bundle/types';
5+
import { getValidRequestBody } from '../../client-core/bundle/utils';
6+
import type {
7+
Client,
8+
Config,
9+
RequestOptions,
10+
ResolvedRequestOptions,
11+
} from './types';
12+
import {
13+
buildOfetchOptions,
14+
buildUrl,
15+
createConfig,
16+
createInterceptors,
17+
isRepeatableBody,
18+
mapParseAsToResponseType,
19+
mergeConfigs,
20+
mergeHeaders,
21+
parseError,
22+
parseSuccess,
23+
setAuthParams,
24+
wrapDataReturn,
25+
wrapErrorReturn,
26+
} from './utils';
27+
28+
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
29+
body?: BodyInit | null | undefined;
30+
headers: ReturnType<typeof mergeHeaders>;
31+
};
32+
33+
export const createClient = (config: Config = {}): Client => {
34+
let _config = mergeConfigs(createConfig(), config);
35+
36+
const getConfig = (): Config => ({ ..._config });
37+
38+
const setConfig = (config: Config): Config => {
39+
_config = mergeConfigs(_config, config);
40+
return getConfig();
41+
};
42+
43+
const interceptors = createInterceptors<
44+
Request,
45+
Response,
46+
unknown,
47+
ResolvedRequestOptions
48+
>();
49+
50+
// Resolve final options, serialized body, network body and URL
51+
const resolveOptions = async (options: RequestOptions) => {
52+
const opts = {
53+
..._config,
54+
...options,
55+
headers: mergeHeaders(_config.headers, options.headers),
56+
serializedBody: undefined,
57+
};
58+
59+
if (opts.security) {
60+
await setAuthParams({
61+
...opts,
62+
security: opts.security,
63+
});
64+
}
65+
66+
if (opts.requestValidator) {
67+
await opts.requestValidator(opts);
68+
}
69+
70+
if (opts.body !== undefined && opts.bodySerializer) {
71+
opts.serializedBody = opts.bodySerializer(opts.body);
72+
}
73+
74+
// remove Content-Type header if body is empty to avoid sending invalid requests
75+
if (opts.body === undefined || opts.serializedBody === '') {
76+
opts.headers.delete('Content-Type');
77+
}
78+
79+
// If user provides a raw body (no serializer), adjust Content-Type sensibly.
80+
// Avoid overriding explicit user-defined headers; only correct the default JSON header.
81+
if (
82+
opts.body !== undefined &&
83+
opts.bodySerializer === null &&
84+
(opts.headers.get('Content-Type') || '').toLowerCase() ===
85+
'application/json'
86+
) {
87+
const b: unknown = opts.body;
88+
if (typeof FormData !== 'undefined' && b instanceof FormData) {
89+
// Let the runtime set proper boundary
90+
opts.headers.delete('Content-Type');
91+
} else if (
92+
typeof URLSearchParams !== 'undefined' &&
93+
b instanceof URLSearchParams
94+
) {
95+
// Set standard urlencoded content type with charset
96+
opts.headers.set(
97+
'Content-Type',
98+
'application/x-www-form-urlencoded;charset=UTF-8',
99+
);
100+
} else if (typeof Blob !== 'undefined' && b instanceof Blob) {
101+
const t = b.type?.trim();
102+
if (t) {
103+
opts.headers.set('Content-Type', t);
104+
} else {
105+
// No known type for the blob: avoid sending misleading JSON header
106+
opts.headers.delete('Content-Type');
107+
}
108+
}
109+
}
110+
111+
// Precompute network body for retries and consistent handling
112+
const networkBody = getValidRequestBody(opts) as
113+
| RequestInit['body']
114+
| null
115+
| undefined;
116+
117+
const url = buildUrl(opts);
118+
119+
return { networkBody, opts, url };
120+
};
121+
122+
// Apply request interceptors to a Request and reflect header/method/signal
123+
const applyRequestInterceptors = async (
124+
request: Request,
125+
opts: ResolvedRequestOptions,
126+
) => {
127+
for (const fn of interceptors.request.fns) {
128+
if (fn) {
129+
request = await fn(request, opts);
130+
}
131+
}
132+
// Reflect any interceptor changes into opts used for network and downstream
133+
opts.headers = request.headers;
134+
opts.method = request.method as Uppercase<HttpMethod>;
135+
// Note: we intentionally ignore request.body changes from interceptors to
136+
// avoid turning serialized bodies into streams. Body is sourced solely
137+
// from getValidRequestBody(options) for consistency.
138+
// Attempt to reflect possible signal changes
139+
opts.signal = (request as any).signal as AbortSignal | undefined;
140+
return request;
141+
};
142+
143+
// Build ofetch options with stable retry logic based on body repeatability
144+
const buildNetworkOptions = (
145+
opts: ResolvedRequestOptions,
146+
body: BodyInit | null | undefined,
147+
responseType: OfetchResponseType | undefined,
148+
) => {
149+
const effectiveRetry = isRepeatableBody(body)
150+
? (opts.retry as any)
151+
: (0 as any);
152+
return buildOfetchOptions(opts, body, responseType, effectiveRetry);
153+
};
154+
155+
const request: Client['request'] = async (options) => {
156+
const {
157+
networkBody: initialNetworkBody,
158+
opts,
159+
url,
160+
} = await resolveOptions(options as any);
161+
// Compute response type mapping once
162+
const ofetchResponseType: OfetchResponseType | undefined =
163+
mapParseAsToResponseType(opts.parseAs, opts.responseType);
164+
165+
const $ofetch = opts.ofetch ?? ofetch;
166+
167+
// Always create Request pre-network (align with client-fetch)
168+
const networkBody = initialNetworkBody;
169+
const requestInit: ReqInit = {
170+
body: networkBody,
171+
headers: opts.headers as Headers,
172+
method: opts.method,
173+
redirect: 'follow',
174+
signal: opts.signal,
175+
};
176+
let request = new Request(url, requestInit);
177+
178+
request = await applyRequestInterceptors(request, opts);
179+
const finalUrl = request.url;
180+
181+
// Build ofetch options and perform the request
182+
const responseOptions = buildNetworkOptions(
183+
opts as ResolvedRequestOptions,
184+
networkBody,
185+
ofetchResponseType,
186+
);
187+
188+
let response = await $ofetch.raw(finalUrl, responseOptions);
189+
190+
for (const fn of interceptors.response.fns) {
191+
if (fn) {
192+
response = await fn(response, request, opts);
193+
}
194+
}
195+
196+
const result = { request, response };
197+
198+
if (response.ok) {
199+
const data = await parseSuccess(response, opts, ofetchResponseType);
200+
return wrapDataReturn(data, result, opts.responseStyle);
201+
}
202+
203+
let finalError = await parseError(response);
204+
205+
for (const fn of interceptors.error.fns) {
206+
if (fn) {
207+
finalError = await fn(finalError, response, request, opts);
208+
}
209+
}
210+
211+
// Ensure error is never undefined after interceptors
212+
finalError = (finalError as any) || ({} as string);
213+
214+
if (opts.throwOnError) {
215+
throw finalError;
216+
}
217+
218+
return wrapErrorReturn(finalError, result, opts.responseStyle) as any;
219+
};
220+
221+
const makeMethodFn =
222+
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
223+
request({ ...options, method } as any);
224+
225+
const makeSseFn =
226+
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
227+
const { networkBody, opts, url } = await resolveOptions(options);
228+
const optsForSse: any = { ...opts };
229+
delete optsForSse.body;
230+
return createSseClient({
231+
...optsForSse,
232+
fetch: opts.fetch,
233+
headers: opts.headers as Headers,
234+
method,
235+
onRequest: async (url, init) => {
236+
let request = new Request(url, init);
237+
request = await applyRequestInterceptors(request, opts);
238+
return request;
239+
},
240+
serializedBody: networkBody as BodyInit | null | undefined,
241+
signal: opts.signal,
242+
url,
243+
});
244+
};
245+
246+
return {
247+
buildUrl,
248+
connect: makeMethodFn('CONNECT'),
249+
delete: makeMethodFn('DELETE'),
250+
get: makeMethodFn('GET'),
251+
getConfig,
252+
head: makeMethodFn('HEAD'),
253+
interceptors,
254+
options: makeMethodFn('OPTIONS'),
255+
patch: makeMethodFn('PATCH'),
256+
post: makeMethodFn('POST'),
257+
put: makeMethodFn('PUT'),
258+
request,
259+
setConfig,
260+
sse: {
261+
connect: makeSseFn('CONNECT'),
262+
delete: makeSseFn('DELETE'),
263+
get: makeSseFn('GET'),
264+
head: makeSseFn('HEAD'),
265+
options: makeSseFn('OPTIONS'),
266+
patch: makeSseFn('PATCH'),
267+
post: makeSseFn('POST'),
268+
put: makeSseFn('PUT'),
269+
trace: makeSseFn('TRACE'),
270+
},
271+
trace: makeMethodFn('TRACE'),
272+
} as Client;
273+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type { Auth } from '../../client-core/bundle/auth';
2+
export type { QuerySerializerOptions } from '../../client-core/bundle/bodySerializer';
3+
export {
4+
formDataBodySerializer,
5+
jsonBodySerializer,
6+
urlSearchParamsBodySerializer,
7+
} from '../../client-core/bundle/bodySerializer';
8+
export { buildClientParams } from '../../client-core/bundle/params';
9+
export { createClient } from './client';
10+
export type {
11+
Client,
12+
ClientOptions,
13+
Config,
14+
CreateClientConfig,
15+
Options,
16+
OptionsLegacyParser,
17+
RequestOptions,
18+
RequestResult,
19+
ResolvedRequestOptions,
20+
ResponseStyle,
21+
TDataShape,
22+
} from './types';
23+
export { createConfig, mergeHeaders } from './utils';

0 commit comments

Comments
 (0)