import { ApiCollection, ApiResource, FailedResponse } from '../../types' import { isResponseLike } from '../response_error' import { concatRecords, ensureNoQueryParamsInDev, makeRequest, MakeRequestArgs, throwErrorIfFieldsMissing, throwErrorIfQueryParams, } from './request_helpers' import { DeleteRequest, GetRequest, PatchRequest, PostRequest, WalkRequest } from './types' export type OnUnauthorizedResponse = (_response: FailedResponse) => void type ClientArgs = { version: string defaultHeaders?: Record onUnauthorizedResponse?: OnUnauthorizedResponse root?: string } export class Client { version: string = '' defaultHeaders: Record = {} onUnauthorizedResponse?: OnUnauthorizedResponse root?: string constructor({ version, defaultHeaders = {}, onUnauthorizedResponse, root }: ClientArgs) { this.version = version this.defaultHeaders = defaultHeaders this.onUnauthorizedResponse = onUnauthorizedResponse this.root = root } async get(args: GetRequest): Promise { const { walk, ...data } = args.data const isWalking = Boolean(walk) const headers = { ...this.headers, ...args.headers } const url = this.appUrl(args.url) await throwErrorIfQueryParams(url) const requestArgs: MakeRequestArgs = { data, url, action: 'GET', headers } const walkRequest = ({ url: requestUrl, data: d = { fields: {} }, acc = { data: [], included: [], meta: {}, links: {} }, ...options }: WalkRequest): Promise => { return makeRequest({ action: 'GET', data: d, url: requestUrl, ...options, headers, }).then(({ links, ...rest }) => { const records = Array.isArray(rest.data) ? concatRecords(acc, rest) : rest // `next` will have our params in the link so we do not want to pass them back in if (links?.next) { return walkRequest({ ...options, data: d, url: links.next, acc: records }) } else { return records } }) } const handler = isWalking ? (a: MakeRequestArgs | WalkRequest) => walkRequest(a as WalkRequest) : (a: MakeRequestArgs | WalkRequest) => makeRequest(a as MakeRequestArgs) return throwErrorIfFieldsMissing(handler, requestArgs) .then(response => response as T) .catch(this.handleNotOk) } async patch(args: PatchRequest): Promise { const headers = { ...this.headers, ...args.headers } const url = this.appUrl(args.url) const requestArgs: MakeRequestArgs = { data: args.data, url, action: 'PATCH', headers } return ensureNoQueryParamsInDev(makeRequest, requestArgs) .then(response => response as T) .catch(this.handleNotOk) } async post(args: PostRequest): Promise { const headers = { ...this.headers, ...args.headers } const url = this.appUrl(args.url) const requestArgs: MakeRequestArgs = { ...args, data: args.data, url, action: 'POST', headers } return ensureNoQueryParamsInDev(makeRequest, requestArgs) .then(response => response as T) .catch(this.handleNotOk) } async delete(args: DeleteRequest) { const headers = { ...this.headers, ...args.headers } const url = this.appUrl(args.url) const requestArgs: MakeRequestArgs = { url, action: 'DELETE', headers } return makeRequest(requestArgs).catch(this.handleNotOk) } handleNotOk = async (response: Response | Error) => { if (!isResponseLike(response)) return Promise.reject(response) const errorData = await this.parseErrorResponse(response) const notOkResponse = response.clone() as FailedResponse notOkResponse.errors = errorData.errors || [] if (response.status === 401) { this.onUnauthorizedResponse?.(notOkResponse) } return Promise.reject(notOkResponse) } parseErrorResponse = async (response: Response): Promise> => { try { return (await response.clone().json()) as FailedResponse } catch { return {} } } appUrl(url: string) { return `${this.root}${url}` } get headers() { return { Accept: 'application/vnd.api+json', 'Content-Type': 'application/json', 'X-PCO-API-Version': this.version, ...this.defaultHeaders, } } } export default Client