Skip to content

Commit

Permalink
feat: deep merge custom fetch options
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Sep 3, 2023
1 parent 2f5f6ca commit 55342cb
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 34 deletions.
31 changes: 12 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ofetch } from 'ofetch'
import { resolveURL, withQuery } from 'ufo'
import { joinURL } from 'ufo'
import type { QueryObject } from 'ufo'
import type { FetchOptions } from 'ofetch'
import type { ClientBuilder, ClientMethodHandler, ResponseType } from './types'
import { headersToObject } from './utils'
import type { ApiBuilder, ApiMethodHandler, ResponseType } from './types'
import { mergeFetchOptions } from './utils'

export type { ClientBuilder }
export type { ApiBuilder }

const payloadMethods = [
'POST',
Expand All @@ -19,46 +19,39 @@ const payloadMethods = [
*/
export function createClient<R extends ResponseType = 'json'>(
defaultOptions: Omit<FetchOptions<R>, 'method'> = {},
): ClientBuilder {
): ApiBuilder {
// Callable internal target required to use `apply` on it
const internalTarget = (() => {}) as ClientBuilder
const internalTarget = (() => {}) as ApiBuilder

function p(url: string): ClientBuilder {
function p(url: string): ApiBuilder {
return new Proxy(internalTarget, {
get(_target, key: string) {
const method = key.toUpperCase()

if (!['GET', ...payloadMethods].includes(method))
return p(resolveURL(url, key))
return p(joinURL(url, key))

const handler: ClientMethodHandler = <T = any, R extends ResponseType = 'json'>(
const handler: ApiMethodHandler = <T = any, R extends ResponseType = 'json'>(
data?: RequestInit['body'] | Record<string, any>,
opts: FetchOptions<R> = {},
) => {
if (method === 'GET' && data)
url = withQuery(url, data as QueryObject)
opts.query = data as QueryObject
else if (payloadMethods.includes(method as typeof payloadMethods[number]))
opts.body = data

opts.method = method

return ofetch<T, R>(
url,
{
...defaultOptions,
...opts,
headers: {
...headersToObject(defaultOptions.headers),
...headersToObject(opts.headers),
},
} as FetchOptions<R>,
mergeFetchOptions(opts, defaultOptions) as FetchOptions<R>,
)
}

return handler
},
apply(_target, _thisArg, args: (string | number)[] = []) {
return p(resolveURL(url, ...args.map(i => `${i}`)))
return p(joinURL(url, ...args.map(i => `${i}`)))
},
})
}
Expand Down
18 changes: 9 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ export interface ResponseMap {
export type ResponseType = keyof ResponseMap | 'json'
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap ? ResponseMap[R] : JsonType

export type ClientMethodHandler = <T = any, R extends ResponseType = 'json'>(
export type ApiMethodHandler = <T = any, R extends ResponseType = 'json'>(
data?: RequestInit['body'] | Record<string, any>,
opts?: Omit<FetchOptions<R>, 'baseURL' | 'method'>
) => Promise<MappedType<R, T>>

export type ClientBuilder = {
[key: string]: ClientBuilder
(...segmentsOrIds: (string | number)[]): ClientBuilder
export type ApiBuilder = {
[key: string]: ApiBuilder
(...segmentsOrIds: (string | number)[]): ApiBuilder
} & {
get: ClientMethodHandler
post: ClientMethodHandler
put: ClientMethodHandler
delete: ClientMethodHandler
patch: ClientMethodHandler
get: ApiMethodHandler
post: ApiMethodHandler
put: ApiMethodHandler
delete: ApiMethodHandler
patch: ApiMethodHandler
}
37 changes: 31 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
export function headersToObject(headers: HeadersInit = {}): Record<string, string> {
if (headers instanceof Headers)
return Object.fromEntries([...headers.entries()])
import type { FetchOptions } from 'ofetch'

if (Array.isArray(headers))
return Object.fromEntries(headers)
export function mergeFetchOptions(
input: FetchOptions | undefined,
defaults: FetchOptions | undefined,
): FetchOptions {
const merged: FetchOptions = {
...defaults,
...input,
}

return headers
// Merge params and query
if (defaults?.params && input?.params) {
merged.params = {
...defaults?.params,
...input?.params,
}
}
if (defaults?.query && input?.query) {
merged.query = {
...defaults?.query,
...input?.query,
}
}

// Merge headers
if (defaults?.headers && input?.headers) {
merged.headers = new Headers(defaults?.headers || {})
for (const [key, value] of new Headers(input?.headers || {}))
merged.headers.set(key, value)
}

return merged
}

0 comments on commit 55342cb

Please sign in to comment.