UNPKG

@web-atoms/core

Version:
345 lines (297 loc) • 11 kB
import { App } from "../App"; import { CancelToken, IDisposable } from "../core/types"; import { BusyIndicatorService } from "./BusyIndicatorService"; type IRequest = { jsonPostProcessor?: (x) => any, dispatcher?: any, fetchProxy?: any, url?: string, hideBusyIndicator?, log?: (...a: any[]) => void, logError?: (...a: any[]) => void } & RequestInit; export default class FetchBuilder { static JsonError: typeof JsonError; static buildUrl(strings: TemplateStringsArray, ... p: any[]) { let r = ""; for (let index = 0; index < strings.length; index++) { const element = strings[index]; r += element; if(index < p.length) { r += encodeURIComponent(p[index]); } } return r; } public static get(url) { return this.method(url, "GET"); } public static put(url) { return this.method(url, "PUT"); } public static post(url) { return this.method(url, "POST"); } public static delete(url) { return this.method(url, "DELETE"); } public static url(url: string) { return this.method(url, "GET"); } public static header(name: string, value: string) { return new FetchBuilder({ headers: { url: "", method: "POST", [name]: value }}); } public static method(url, method: string) { return new FetchBuilder({ url, method }); } private constructor(private readonly request: IRequest) { } public log(logger: (...a: any[]) => void) { return this.append({ log: logger }); } public logWhenFailed(logger: (...a: any[]) => void) { return this.append({ logError: logger }); } public get(url) { return this.method(url, "GET"); } public put(url) { return this.method(url, "PUT"); } public post(url) { return this.method(url, "POST"); } public delete(url) { return this.method(url, "DELETE"); } public method(url: string, method: string ) { return this.append({ url, method }); } // public cancelToken(cancelToken: CancelToken) { // const ac = new AbortController(); // cancelToken.registerForCancel(() => ac.abort()); // return this.signal(ac.signal); // } public signal(signal: AbortSignal) { if (!signal) { return this; } return this.append({ signal }); } public cancelToken(ct: CancelToken) { if (!ct) { return this; } const ac = new AbortController(); const signal = ac.signal; ct.registerForCancel(() => ac.abort()); return this.signal(signal); } public form(name: string, value: string): FetchBuilder; public form(name: string, value: Blob, fileName: string): FetchBuilder; public form(name: string, value: string | Blob, fileName?: string ): FetchBuilder { if (value === void 0) { return this; } const body = this.request.body as FormData ?? new FormData(); if (fileName) { if (typeof value === "string") { throw new Error("value must be a blob with content type set correctly."); } body.append(name, value as Blob, fileName) } else { body.append(name, value); } return this.append ({ body }); } public jsonBody(body, encode = true) { if (encode) { body = JSON.stringify(body); } const headers = this.request.headers ?? {}; headers["content-type"] = "application/json"; return this.append ({ body, headers }); } public header(name: string, value: string) { const headers = this.request.headers ?? {}; headers[name] = value; return this.append({ headers }); } public path(name: string, value: any, encode = true) { let url = this.request.url; if (encode) { value = encodeURIComponent(value); } url = url.replace(name, value); return this.append({ url }); } public query(name: any, value: any, encode = true) { if (value === void 0) { return this; } let url = this.request.url; if (encode) { value = encodeURIComponent(value); } name = encodeURIComponent(name); if (url.indexOf("?") === -1) { url += `?${name}=${value}`; } else { url += `&${name}=${value}`; } return this.append({ url }); } public queries(obj: { [key: string]: any}, encode = true, encodeObjectAsJson = true) { let url = this.request.url; let prefix = url.indexOf("?") === -1 ? "?" : "&"; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { let value = obj[key]; if (value === void 0) { continue; } if (encodeObjectAsJson) { if (typeof value === "object" && value !== null) { value = JSON.stringify(value); } encode = true; } if (encode) { value = encodeURIComponent(value); } const name = encodeURIComponent(key); url += `${prefix}${name}=${value}`; prefix = "&" } } return this.append({ url }); } public async asText(ensureSuccess = true) { const { result } = await this.asTextResponse(ensureSuccess); return result; } public async asBlob(ensureSuccess = true) { const { result } = await this.asBlobResponse(ensureSuccess); return result; } public async asJson<T = any>(ensureSuccess = true) { const { result } = await this.asJsonResponse<T>(ensureSuccess); return result; } public jsonPostProcessor(jsonPostProcessor: (x) => any) { return this.append({ jsonPostProcessor }); } public async asJsonResponse<T = any>(ensureSuccess = true) { return this.execute<T>(ensureSuccess, async (x, jsonPostProcessor) => { if(!/json/i.test(x.headers.get("content-type"))) { throw new Error(`Failed to parse json from ${this.request.url}\n${await x.text()}`); } if (jsonPostProcessor) { return x.json().then(jsonPostProcessor) as T; } return x.json() as T; }); } public async asTextResponse(ensureSuccess = true) { return this.execute(ensureSuccess, (x) => x.text()); } public asBlobResponse(ensureSuccess = true) { return this.execute(ensureSuccess, (x) => x.blob()); } public dispatcher(dispatcher: any) { return this.append({ dispatcher }); } public withFetchProxy(fetchProxy: any) { return this.append({ fetchProxy }); } public async execute<T>(ensureSuccess = true, postProcessor: (r: Response, next?: (data) => any) => T | Promise<T>): Promise<{ result: T, headers: any, status: number }> { let { log, logError, hideBusyIndicator } = this.request; using _d = !hideBusyIndicator ? App.current.createBusyIndicator() : null; try { const { headers, fetchProxy, jsonPostProcessor } = this.request; const r = await (fetchProxy ?? fetch)(this.request.url, this.request); if (ensureSuccess) { if (r.status > 300) { log = logError; log?.(`fetch: ${this.request.method ?? "GET"} ${this.request.url}`); if (log && headers) { for (const key in headers) { if (headers.hasOwnProperty(key)) { log?.(`${key}: ${headers[key]}`); } } } log?.(`${r.status} ${r.statusText || "Http Error"}`); const type = r.headers.get("content-type"); if (/\/json/i.test(type)) { const json: any = await r.json(); log?.(json); const message = json.title ?? json.detail ?? json.message ?? json.exceptionMessage ?? "Json Server Error"; log = null; logError = null; throw new JsonError(message, json); } const text = await r.text(); log?.(text); log = null; logError = null; throw new Error(`Fetch failed with error ${r.status} for ${this.request.url}\n${text}`); } } log?.(`${this.request.method ?? "GET"} ${this.request.url}`); if (log && headers) { for (const key in headers) { if (headers.hasOwnProperty(key)) { log?.(`${key}: ${headers[key]}`); } } } const result = await postProcessor(r, jsonPostProcessor); if (log) { log(`${r.status} ${r.statusText || "OK"}`) log(result); } return { result, headers: r.headers, status: r.status }; } catch (error) { log?.(error); throw error; } } private append(r: IRequest) { // we will try to merge url here.. let { url } = this.request; const { url: newUrl } = r; if (newUrl) { if (!url) { url = newUrl; } else { if (/^https?\:\/\//i.test(newUrl)) { url = newUrl; } else { let fullUrl = url; if (!/^https?\:\/\//i.test(url)) { fullUrl = new URL(url, location.href).toString(); } url = (new URL(newUrl, fullUrl)).toString(); } } } return new FetchBuilder({ ... this.request, ... r, url }); } } class JsonError extends Error { constructor(message, public readonly json) { super(message); } } FetchBuilder.JsonError = JsonError;