generated from un-ts/lib-boilerplate
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
375 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,147 @@ | ||
export default () => 'Hello World!' | ||
import type { URLSearchParametersOptions, ValueOf } from './types.js' | ||
import { CONTENT_TYPE, isPlainObject, normalizeUrl } from './utils.js' | ||
|
||
export type * from './types.js' | ||
export * from './utils.js' | ||
|
||
export const ApiMethod = { | ||
GET: 'GET', | ||
POST: 'POST', | ||
PATCH: 'PATCH', | ||
PUT: 'PUT', | ||
DELETE: 'DELETE', | ||
} as const | ||
|
||
export type ApiMethod = ValueOf<typeof ApiMethod> | ||
|
||
export interface FetchApiOptions extends Omit<RequestInit, 'body' | 'method'> { | ||
method?: ApiMethod | ||
body?: BodyInit | object | ||
query?: URLSearchParametersOptions | ||
json?: boolean | ||
type?: 'arrayBuffer' | 'blob' | 'json' | 'text' | null | ||
} | ||
|
||
export interface InterceptorRequest extends FetchApiOptions { | ||
url: string | ||
} | ||
|
||
export type ApiInterceptor = ( | ||
request: InterceptorRequest, | ||
next: (request: InterceptorRequest) => PromiseLike<Response>, | ||
) => PromiseLike<Response> | Response | ||
|
||
export interface ResponseError<T = never> extends Error { | ||
data?: T | null | ||
response?: Response | null | ||
} | ||
|
||
export class ApiInterceptors { | ||
readonly #interceptors: ApiInterceptor[] = [] | ||
|
||
get length() { | ||
return this.#interceptors.length | ||
} | ||
|
||
at(index: number) { | ||
return this.#interceptors.at(index) | ||
} | ||
|
||
use(...interceptors: ApiInterceptor[]) { | ||
this.#interceptors.push(...interceptors) | ||
return this | ||
} | ||
|
||
eject(interceptor: ApiInterceptor) { | ||
const index = this.#interceptors.indexOf(interceptor) | ||
if (index > -1) { | ||
this.#interceptors.splice(index, 1) | ||
return true | ||
} | ||
return false | ||
} | ||
} | ||
|
||
export const createFetchApi = () => { | ||
const interceptors = new ApiInterceptors() | ||
|
||
function fetchApi( | ||
url: string, | ||
options: FetchApiOptions & { type: null }, | ||
): Promise<Response> | ||
function fetchApi( | ||
url: string, | ||
options: FetchApiOptions & { type: 'arraybuffer' }, | ||
): Promise<ArrayBuffer> | ||
function fetchApi( | ||
url: string, | ||
options: FetchApiOptions & { type: 'blob' }, | ||
): Promise<Blob> | ||
function fetchApi( | ||
url: string, | ||
options: FetchApiOptions & { type: 'text' }, | ||
): Promise<string> | ||
function fetchApi<T>( | ||
url: string, | ||
options?: FetchApiOptions & { type?: 'json' }, | ||
): Promise<T> | ||
// eslint-disable-next-line sonarjs/cognitive-complexity | ||
async function fetchApi( | ||
url: string, | ||
{ | ||
method = ApiMethod.GET, | ||
body, | ||
headers, | ||
json = body != null && (isPlainObject(body) || Array.isArray(body)), | ||
type = 'json', | ||
...rest | ||
}: FetchApiOptions = {}, | ||
) { | ||
headers = new Headers(headers) | ||
|
||
if (json && !headers.has(CONTENT_TYPE)) { | ||
headers.append(CONTENT_TYPE, 'application/json') | ||
} | ||
|
||
let index = 0 | ||
|
||
const next = async (request: InterceptorRequest) => { | ||
if (index < interceptors.length) { | ||
return interceptors.at(index++)!(request, next) | ||
} | ||
const { body, url, query, ...rest } = request | ||
const response = await fetch(normalizeUrl(url, query), { | ||
...rest, | ||
body: json ? JSON.stringify(body) : (body as BodyInit), | ||
}) | ||
if (response.ok) { | ||
return response | ||
} | ||
let data: unknown = null | ||
if (type != null) { | ||
try { | ||
data = await response.clone()[type]() | ||
} catch { | ||
data = await response.clone().text() | ||
} | ||
} | ||
throw Object.assign(new Error(response.statusText), { | ||
data, | ||
response, | ||
}) | ||
} | ||
|
||
const response = await next({ | ||
url, | ||
method, | ||
body, | ||
headers, | ||
...rest, | ||
}) | ||
return type == null ? response : response.clone()[type]() | ||
} | ||
|
||
return { interceptors, fetchApi } | ||
} | ||
|
||
export const { interceptors, fetchApi } = createFetchApi() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export type Nullable<T> = T | null | undefined | ||
|
||
export type ValueOf<T> = T[keyof T] | ||
|
||
export type URLSearchParametersInit = ConstructorParameters< | ||
typeof URLSearchParams | ||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers | ||
>[0] | ||
|
||
export type URLSearchParametersOptions = | ||
| Record<string, Nullable<number | string>> | ||
| URLSearchParametersInit | ||
| object |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { | ||
Nullable, | ||
URLSearchParametersInit, | ||
URLSearchParametersOptions, | ||
ValueOf, | ||
} from './types.js' | ||
|
||
export const CONTENT_TYPE = 'Content-Type' | ||
|
||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
const { toString } = Object.prototype // type-coverage:ignore-line - TODO: report bug | ||
|
||
const objectTag = '[object Object]' | ||
|
||
export const isPlainObject = <T extends object>(value: unknown): value is T => | ||
toString.call(value) === objectTag | ||
|
||
export const cleanNilValues = <T = unknown>(input: T, empty?: boolean): T => { | ||
if (!isPlainObject(input)) { | ||
return input | ||
} | ||
|
||
for (const _key of Object.keys(input)) { | ||
const key = _key as keyof T | ||
const value = input[key] as Nullable<ValueOf<T>> | ||
if (empty ? !value : value == null) { | ||
delete input[key] | ||
} else { | ||
input[key] = cleanNilValues(value, empty) as (T & object)[keyof T] | ||
} | ||
} | ||
|
||
return input | ||
} | ||
|
||
export const normalizeUrl = ( | ||
url: string, | ||
query?: URLSearchParametersOptions, | ||
) => { | ||
const search = new URLSearchParams( | ||
cleanNilValues(query, true) as URLSearchParametersInit, | ||
).toString() | ||
return search ? url + (url.includes('?') ? '&' : '?') + search : url | ||
} |
Oops, something went wrong.