Files
remark42/frontend/packages/api/lib/fetcher.ts
T
2022-08-20 12:58:40 -05:00

95 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { JWT_HEADER, XSRF_COOKIE, XSRF_HEADER } from '../consts'
import { getCookie } from '../lib/cookies'
export type QueryParams = Record<string, string | number | string[] | number[] | undefined>
export type Payload = BodyInit | Record<string, unknown> | null
export type BodylessMethod = <T>(url: string, query?: QueryParams) => Promise<T>
export type BodyMethod = <T>(url: string, query?: QueryParams, body?: Payload) => Promise<T>
export interface Client {
get: BodylessMethod
put: BodyMethod
post: BodyMethod
delete: BodylessMethod
}
/** JWT token received from server and will be send by each request, if it present */
let token: string | undefined
export const createFetcher = (site: string, baseUrl: string): Client => {
const client = {
get: <T>(uri: string, query?: QueryParams): Promise<T> => request<T>('get', uri, query),
put: <T>(uri: string, query?: QueryParams, body?: Payload): Promise<T> =>
request<T>('put', uri, query, body),
post: <T>(uri: string, query?: QueryParams, body?: Payload): Promise<T> =>
request<T>('post', uri, query, body),
delete: <T>(uri: string, query?: QueryParams, body?: Payload): Promise<T> =>
request<T>('delete', uri, query, body),
}
/**
* Fetcher is abstraction on top of fetch
*
* @method - a string to set http method
* @uri uri to API endpoint
* @query - collection of query params. They will be concatenated to URL. `siteId` will be added automatically.
* @body - data for sending to the server. If you pass object it will be stringified. If you pass form data it will be sent as is. Content type headers will be added automatically.
*/
async function request<T>(
method: string,
uri: string,
query: QueryParams = {},
body?: Payload
): Promise<T> {
const searchParams = new URLSearchParams({ site, ...query })
searchParams.sort()
const url = `${baseUrl}${uri}?${searchParams.toString()}`
const headers = new Headers()
const params: RequestInit = { method, headers }
// Save token in memory and pass it into headers in case if storing cookies is disabled
if (token) {
headers.set(JWT_HEADER, token)
}
// An HTTP header cannot be empty.
// Although some webservers allow this (nginx, Apache), others answer 400 Bad Request (lighttpd).
const xsrfToken = getCookie(XSRF_COOKIE)
if (typeof xsrfToken === 'string') {
headers.set(XSRF_HEADER, xsrfToken)
}
if (typeof body === 'object' && body !== null && !(body instanceof FormData)) {
headers.set('Content-Type', 'application/json')
params.body = JSON.stringify(body)
} else {
params.body = body
}
return fetch(url, params).then<T>((res) => {
if ([401, 403].includes(res.status)) {
token = undefined
return Promise.reject('Unauthorized')
}
token = res.headers.get(JWT_HEADER) ?? token
return res
.text()
.catch(Object)
.then((data: string) => {
if (res.status < 200 || res.status > 299) {
return Promise.reject(data)
}
try {
return JSON.parse(data) as T
} catch (e) {
return data as unknown as T
}
})
})
}
return client
}