Skip to content

Commit bc40406

Browse files
committed
feat: custom open fetch by ofetch
1 parent 4fec6a0 commit bc40406

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/utils/__tests__/fetch.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it } from 'vitest'
2+
import { createOpenFetch } from '../fetch'
3+
import type { paths } from '#/generated/api-schema'
4+
5+
describe('fetch', () => {
6+
it('should fill path correctly', async () => {
7+
const $fetch = createOpenFetch<paths>({
8+
baseURL: 'https://api.example.com',
9+
})
10+
11+
await $fetch('/auth/login', {
12+
method: 'POST',
13+
body: {
14+
15+
password: 'password',
16+
},
17+
})
18+
19+
await $fetch('/posts/{postId}', {
20+
method: 'GET',
21+
path: {
22+
postId: 1,
23+
},
24+
})
25+
})
26+
})

src/utils/fetch.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
$fetch,
3+
type FetchContext,
4+
type FetchError,
5+
type FetchOptions,
6+
} from 'ofetch'
7+
import type {
8+
ErrorResponse,
9+
MediaType,
10+
OperationRequestBodyContent,
11+
ResponseObjectMap,
12+
SuccessResponse,
13+
} from 'openapi-typescript-helpers'
14+
15+
export type FetchResponseData<
16+
T,
17+
Media extends MediaType = MediaType,
18+
> = SuccessResponse<ResponseObjectMap<T>, Media>
19+
20+
export type FetchResponseError<
21+
T,
22+
Media extends MediaType = MediaType,
23+
> = FetchError<ErrorResponse<ResponseObjectMap<T>, Media>>
24+
25+
export type MethodOption<M, P> = 'get' extends keyof P
26+
? { method?: M }
27+
: { method: M }
28+
29+
export type ParamsOption<T> = T extends { parameters?: any; query?: any }
30+
? T['parameters']
31+
: Record<string, never>
32+
33+
export type RequestBodyOption<T> =
34+
OperationRequestBodyContent<T> extends never
35+
? { body?: never }
36+
: undefined extends OperationRequestBodyContent<T>
37+
? { body?: OperationRequestBodyContent<T> }
38+
: { body: OperationRequestBodyContent<T> }
39+
40+
export type FilterMethods<T> = {
41+
[K in keyof Omit<T, 'parameters'> as T[K] extends never | undefined
42+
? never
43+
: K]: T[K]
44+
}
45+
46+
export type OpenFetchOptions<
47+
Method,
48+
LowercasedMethod,
49+
Params,
50+
Operation = 'get' extends LowercasedMethod
51+
? 'get' extends keyof Params
52+
? Params['get']
53+
: never
54+
: LowercasedMethod extends keyof Params
55+
? Params[LowercasedMethod]
56+
: never,
57+
> = MethodOption<Method, Params> &
58+
ParamsOption<Operation> &
59+
RequestBodyOption<Operation> &
60+
Omit<FetchOptions, 'query' | 'body' | 'method'>
61+
62+
export type OpenFetchClient<Paths> = <
63+
ReqT extends Extract<keyof Paths, string>,
64+
Methods extends FilterMethods<Paths[ReqT]>,
65+
Method extends
66+
| Extract<keyof Methods, string>
67+
| Uppercase<Extract<keyof Methods, string>>,
68+
LowercasedMethod extends Lowercase<Method> extends keyof FilterMethods<
69+
Paths[ReqT]
70+
>
71+
? Lowercase<Method>
72+
: never,
73+
DefaultMethod extends 'get' extends LowercasedMethod
74+
? 'get'
75+
: LowercasedMethod,
76+
ResT = FetchResponseData<Paths[ReqT][DefaultMethod]>,
77+
ErrorT = FetchResponseError<Methods[DefaultMethod]>,
78+
>(
79+
url: ReqT,
80+
options?: OpenFetchOptions<Method, LowercasedMethod, Methods>,
81+
) => Promise<ResT | ErrorT>
82+
83+
// More flexible way to rewrite the request path,
84+
// but has problems - https://github.com/unjs/ofetch/issues/319
85+
export function openFetchRequestInterceptor(ctx: FetchContext) {
86+
ctx.request = fillPath(
87+
ctx.request as string,
88+
(ctx.options as { path: Record<string, string> }).path,
89+
)
90+
}
91+
92+
export function createOpenFetch<Paths>(
93+
options: FetchOptions | ((options: FetchOptions) => FetchOptions),
94+
): OpenFetchClient<Paths> {
95+
return (url: string, opts: any = {}) => {
96+
return $fetch(
97+
fillPath(url, opts?.path),
98+
typeof options === 'function'
99+
? options(opts)
100+
: {
101+
...options,
102+
...opts,
103+
},
104+
)
105+
}
106+
}
107+
108+
function fillPath(path: string, params: object = {}) {
109+
for (const [k, v] of Object.entries(params))
110+
path = path.replace(`{${k}}`, encodeURIComponent(String(v)))
111+
return path
112+
}

0 commit comments

Comments
 (0)