@volverjs/data
Version:
Repository pattern implementation with a tiny HttpClient based on Fetch API.
260 lines (235 loc) • 7.9 kB
text/typescript
import type { Hooks, KyResponse, Options, ResponsePromise } from 'ky'
import type { KyInstance } from 'ky/distribution/types/ky'
import type {
HttpMethod,
Input,
KyHeadersInit,
RetryOptions,
} from 'ky/distribution/types/options'
import type { ParamMap } from './types'
import type { UrlBuilderInstance, UrlBuilderOptions } from './UrlBuilder'
import ky, {
HTTPError,
TimeoutError,
} from 'ky'
import {
UrlBuilder,
} from './UrlBuilder'
export type HttpClientResponse = KyResponse
export type HttpClientResponsePromise = ResponsePromise
export type HttpClientOptions = Omit<Options, 'searchParams'> & {
searchParams?: UrlBuilderOptions
}
export type HttpClientRequestOptions = HttpClientOptions & {
abortController?: AbortController
}
export type HttpClientMethod = HttpMethod
export type HttpClientHeaders = KyHeadersInit
export type HttpClientRetryOptions = RetryOptions
export type HttpClientHooks = Hooks
export type HttpClientInput = Input
export type HttpClientUrlTemplate = {
template: string
params: ParamMap
}
export type HttpClientInputTemplate = Input | HttpClientUrlTemplate
export interface HttpClientInstance {
get: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
post: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
put: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
delete: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
patch: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
head: (
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => HttpClientResponsePromise
extend: (options: HttpClientOptions) => void
clone: (options?: HttpClientOptions) => HttpClientInstance
fetch: (request: Request) => HttpClientResponsePromise
request: (
method: HttpClientMethod,
url: HttpClientInputTemplate,
options?: HttpClientOptions,
) => {
responsePromise: HttpClientResponsePromise
abort: (reason?: string) => void
signal: AbortSignal
}
setBearerToken: (token: string) => void
buildUrl: (
url: HttpClientInputTemplate,
options?: UrlBuilderOptions,
) => HttpClientInput
}
export type HttpClientInstanceOptions = HttpClientOptions & {
client?: KyInstance
urlBuilder?: UrlBuilderInstance
}
export { HTTPError, TimeoutError }
export class HttpClient implements HttpClientInstance {
private _client: KyInstance
private _urlBuilder: UrlBuilderInstance
private _prefixUrl: string | URL | undefined
constructor(options: HttpClientInstanceOptions = {}) {
const { client, urlBuilder, searchParams, ...clientOptions } = options
this._client = client ?? ky.create(clientOptions)
this._urlBuilder = urlBuilder ?? new UrlBuilder(searchParams)
this._prefixUrl = clientOptions.prefixUrl
}
public get = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.get(this.buildUrl(url, searchParams), clientOptions)
}
public post = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.post(
this.buildUrl(url, searchParams),
clientOptions,
)
}
public put = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.put(this.buildUrl(url, searchParams), clientOptions)
}
public delete = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.delete(
this.buildUrl(url, searchParams),
clientOptions,
)
}
public patch = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.patch(
this.buildUrl(url, searchParams),
clientOptions,
)
}
public head = (
url: HttpClientInputTemplate,
options: HttpClientOptions = {},
) => {
const { searchParams, ...clientOptions } = options
return this._client.head(
this.buildUrl(url, searchParams),
clientOptions,
)
}
public request = (
method: HttpClientMethod,
url: HttpClientInputTemplate,
options: HttpClientRequestOptions = {},
) => {
const { abortController, ...otherOptions } = options
const { controller, signal }
= HttpClient.createAbortController(abortController)
return {
responsePromise: this[method](url, { signal, ...otherOptions }),
abort: (reason?: string) => controller.abort(reason),
signal,
}
}
public fetch = (request: Request) => {
return this._client(request)
}
public extend = (options: HttpClientOptions = {}) => {
const { searchParams, ...clientOptions } = options
this._client = this._client.extend(clientOptions)
this._prefixUrl = clientOptions.prefixUrl ?? this._prefixUrl
this._urlBuilder.extend(searchParams ?? {})
}
public clone = (options: HttpClientOptions = {}) => {
const { searchParams, ...clientOptions } = options
return new HttpClient({
client: this._client.extend(clientOptions),
urlBuilder: this._urlBuilder.clone(searchParams),
})
}
public setBearerToken = (
token: string | undefined | null,
{ headerName = 'Authorization', prefix = 'Bearer' } = {},
) => {
this.extend({
headers: {
[headerName]: token ? `${prefix} ${token}` : undefined,
},
})
}
public get stop() {
return this._client.stop
}
public buildUrl(
url: HttpClientInputTemplate,
options?: UrlBuilderOptions,
): HttpClientInput {
const toReturn = HttpClient.buildUrl(url, options, this._urlBuilder)
if (
this._prefixUrl
&& typeof toReturn === 'string'
&& toReturn?.startsWith('/')
) {
return toReturn.slice(1)
}
return toReturn
}
public static readonly buildUrl = (
url: HttpClientInputTemplate,
options?: UrlBuilderOptions,
builder?: UrlBuilderInstance,
) => {
if (HttpClient.isUrlTemplate(url)) {
const { template, params } = url as HttpClientUrlTemplate
return builder
? builder.build(template, params, options)
: UrlBuilder.build(template, params, options)
}
return url as HttpClientInput
}
private static isUrlTemplate(url: HttpClientInputTemplate) {
return (
typeof url === 'object'
&& url !== null
&& ((arrayOfKeys: string[]) =>
['template', 'params'].every(key =>
arrayOfKeys.includes(key),
))(Object.keys(url))
)
}
public static readonly createAbortController = (
abortController?: AbortController,
) => {
const controller = abortController ?? new AbortController()
const { signal } = controller
return { controller, signal }
}
}