2023-02-07 23:22:32 +01:00
|
|
|
import type { Hono } from '../hono.ts'
|
2023-02-13 22:21:30 +01:00
|
|
|
import type { ValidationTargets } from '../types.ts'
|
2023-09-19 00:25:34 +02:00
|
|
|
import { serialize } from '../utils/cookie.ts'
|
2023-02-07 23:22:32 +01:00
|
|
|
import type { UnionToIntersection } from '../utils/types.ts'
|
2023-05-28 01:39:36 +02:00
|
|
|
import type { Callback, Client, ClientRequestOptions } from './types.ts'
|
2023-08-18 09:18:21 +02:00
|
|
|
import { deepMerge, mergePath, removeIndexString, replaceUrlParam } from './utils.ts'
|
2023-02-07 23:22:32 +01:00
|
|
|
|
|
|
|
const createProxy = (callback: Callback, path: string[]) => {
|
|
|
|
const proxy: unknown = new Proxy(() => {}, {
|
|
|
|
get(_obj, key) {
|
2024-01-09 22:45:00 +01:00
|
|
|
if (typeof key !== 'string' || key === 'then') return undefined
|
2023-02-07 23:22:32 +01:00
|
|
|
return createProxy(callback, [...path, key])
|
|
|
|
},
|
|
|
|
apply(_1, _2, args) {
|
|
|
|
return callback({
|
|
|
|
path,
|
|
|
|
args,
|
|
|
|
})
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return proxy
|
|
|
|
}
|
|
|
|
|
|
|
|
class ClientRequestImpl {
|
|
|
|
private url: string
|
|
|
|
private method: string
|
|
|
|
private queryParams: URLSearchParams | undefined = undefined
|
|
|
|
private pathParams: Record<string, string> = {}
|
|
|
|
private rBody: BodyInit | undefined
|
|
|
|
private cType: string | undefined = undefined
|
|
|
|
|
|
|
|
constructor(url: string, method: string) {
|
|
|
|
this.url = url
|
|
|
|
this.method = method
|
|
|
|
}
|
|
|
|
fetch = (
|
2023-02-13 22:21:30 +01:00
|
|
|
args?: ValidationTargets & {
|
2023-02-07 23:22:32 +01:00
|
|
|
param?: Record<string, string>
|
|
|
|
},
|
2023-05-28 01:39:36 +02:00
|
|
|
opt?: ClientRequestOptions
|
2023-02-07 23:22:32 +01:00
|
|
|
) => {
|
|
|
|
if (args) {
|
|
|
|
if (args.query) {
|
|
|
|
for (const [k, v] of Object.entries(args.query)) {
|
2023-08-18 09:18:21 +02:00
|
|
|
if (v === undefined) {
|
2023-08-23 02:11:25 +02:00
|
|
|
continue
|
2023-08-18 09:18:21 +02:00
|
|
|
}
|
|
|
|
|
2023-02-07 23:22:32 +01:00
|
|
|
this.queryParams ||= new URLSearchParams()
|
2023-03-16 13:49:28 +01:00
|
|
|
if (Array.isArray(v)) {
|
|
|
|
for (const v2 of v) {
|
|
|
|
this.queryParams.append(k, v2)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.queryParams.set(k, v)
|
|
|
|
}
|
2023-02-07 23:22:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.queries) {
|
|
|
|
for (const [k, v] of Object.entries(args.queries)) {
|
|
|
|
for (const v2 of v) {
|
|
|
|
this.queryParams ||= new URLSearchParams()
|
|
|
|
this.queryParams.append(k, v2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.form) {
|
|
|
|
const form = new FormData()
|
|
|
|
for (const [k, v] of Object.entries(args.form)) {
|
|
|
|
form.append(k, v)
|
|
|
|
}
|
|
|
|
this.rBody = form
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.json) {
|
|
|
|
this.rBody = JSON.stringify(args.json)
|
|
|
|
this.cType = 'application/json'
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.param) {
|
|
|
|
this.pathParams = args.param
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let methodUpperCase = this.method.toUpperCase()
|
|
|
|
let setBody = !(methodUpperCase === 'GET' || methodUpperCase === 'HEAD')
|
|
|
|
|
2023-09-19 00:25:34 +02:00
|
|
|
const headerValues: Record<string, string> = {
|
|
|
|
...(args?.header ?? {}),
|
|
|
|
...(opt?.headers ? opt.headers : {}),
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args?.cookie) {
|
|
|
|
const cookies: string[] = []
|
|
|
|
for (const [key, value] of Object.entries(args.cookie)) {
|
|
|
|
cookies.push(serialize(key, value, { path: '/' }))
|
|
|
|
}
|
|
|
|
headerValues['Cookie'] = cookies.join(',')
|
|
|
|
}
|
|
|
|
|
2023-02-07 23:22:32 +01:00
|
|
|
if (this.cType) headerValues['Content-Type'] = this.cType
|
|
|
|
|
|
|
|
const headers = new Headers(headerValues ?? undefined)
|
|
|
|
let url = this.url
|
|
|
|
|
|
|
|
url = removeIndexString(url)
|
|
|
|
url = replaceUrlParam(url, this.pathParams)
|
|
|
|
|
|
|
|
if (this.queryParams) {
|
|
|
|
url = url + '?' + this.queryParams.toString()
|
|
|
|
}
|
|
|
|
methodUpperCase = this.method.toUpperCase()
|
|
|
|
setBody = !(methodUpperCase === 'GET' || methodUpperCase === 'HEAD')
|
|
|
|
|
|
|
|
// Pass URL string to 1st arg for testing with MSW and node-fetch
|
2023-02-23 08:26:25 +01:00
|
|
|
return (opt?.fetch || fetch)(url, {
|
2023-02-07 23:22:32 +01:00
|
|
|
body: setBody ? this.rBody : undefined,
|
|
|
|
method: methodUpperCase,
|
|
|
|
headers: headers,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-20 00:56:29 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2023-08-18 09:13:04 +02:00
|
|
|
export const hc = <T extends Hono<any, any, any>>(
|
|
|
|
baseUrl: string,
|
|
|
|
options?: ClientRequestOptions
|
|
|
|
) =>
|
2023-09-09 09:15:13 +02:00
|
|
|
createProxy((opts) => {
|
2023-02-07 23:22:32 +01:00
|
|
|
const parts = [...opts.path]
|
|
|
|
|
|
|
|
let method = ''
|
|
|
|
if (/^\$/.test(parts[parts.length - 1])) {
|
|
|
|
const last = parts.pop()
|
|
|
|
if (last) {
|
|
|
|
method = last.replace(/^\$/, '')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const path = parts.join('/')
|
|
|
|
const url = mergePath(baseUrl, path)
|
2023-09-02 00:15:59 +02:00
|
|
|
if (method === 'url') {
|
2024-01-03 11:53:38 +01:00
|
|
|
if (opts.args[0] && opts.args[0].param) {
|
|
|
|
return new URL(replaceUrlParam(url, opts.args[0].param))
|
|
|
|
}
|
2023-09-02 00:15:59 +02:00
|
|
|
return new URL(url)
|
|
|
|
}
|
|
|
|
|
2023-02-07 23:22:32 +01:00
|
|
|
const req = new ClientRequestImpl(url, method)
|
|
|
|
if (method) {
|
2023-02-20 00:22:01 +01:00
|
|
|
options ??= {}
|
2023-05-28 01:39:36 +02:00
|
|
|
const args = deepMerge<ClientRequestOptions>(options, { ...(opts.args[1] ?? {}) })
|
2023-02-20 00:22:01 +01:00
|
|
|
return req.fetch(opts.args[0], args)
|
2023-02-07 23:22:32 +01:00
|
|
|
}
|
|
|
|
return req
|
|
|
|
}, []) as UnionToIntersection<Client<T>>
|