UNPKG

@thepassle/app-tools

Version:

Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not. Most of these are thin wrappers around native API's, like the native `<dialog>` element, `fetch` API, and `URLPattern`.

207 lines (193 loc) 5.96 kB
import { createLogger } from "../utils/log.js"; const log = createLogger("api"); class StatusError extends Error { constructor(response) { super(response.statusText); this.response = response; } } function handleStatus(response) { if (!response.ok) { log("Response not ok", response); throw new StatusError(response); } return response; } /** @typedef {import('./types.js').Config} Config */ /** @typedef {import('./types.js').Method} Method */ /** @typedef {import('./types.js').Plugin} Plugin */ /** @typedef {import('./types.js').CustomRequestOptions} CustomRequestOptions */ /** @typedef {import('./types.js').RequestOptions} RequestOptions */ /** @typedef {import('./types.js').MetaParams} MetaParams */ /** * @example * const api = new Api({ * baseURL: 'https://api.foo.com/', * responseType: 'text', * plugins: [ * { * beforeFetch: ({url, method, opts, data}) => {}, * afterFetch: ({response}) => response, * } * ] *}); */ export class Api { /** @param {Config} config */ constructor(config = {}) { this.config = { plugins: [], responseType: "json", ...config, }; } /** * @param {string} url * @param {Method} method * @param {RequestOptions} [opts] * @param {object} [data] * @returns */ async fetch(url, method, opts, data) { const plugins = [...this.config.plugins, ...(opts?.plugins || [])]; let fetchFn = globalThis.fetch; let baseURL = opts?.baseURL ?? this.config?.baseURL ?? ""; let responseType = opts?.responseType ?? this.config.responseType; let headers = new Headers({ "Content-Type": "application/json", ...opts?.headers, }); if (baseURL) { url = url.replace(/^(?!.*\/\/)\/?/, baseURL + "/"); } if (opts?.params) { url += `${~url.indexOf("?") ? "&" : "?"}${new URLSearchParams(opts.params)}`; } for (const plugin of plugins) { try { const overrides = await plugin?.beforeFetch?.({ responseType, headers, fetchFn, baseURL, url, method, opts, data, }); if (overrides) { ({ responseType, headers, fetchFn, baseURL, url, method, opts, data, } = { ...overrides }); } } catch (e) { log(`Plugin "${plugin.name}" error on afterFetch hook`); throw e; } } log(`Fetching ${method} ${url}`, { responseType, // @ts-ignore headers: Object.fromEntries(headers), fetchFn, baseURL, url, method, opts, data, }); return ( fetchFn(url, { method, headers, ...(data ? { body: JSON.stringify(data) } : {}), ...(opts?.mode ? { mode: opts.mode } : {}), ...(opts?.credentials ? { credentials: opts.credentials } : {}), ...(opts?.cache ? { cache: opts.cache } : {}), ...(opts?.redirect ? { redirect: opts.redirect } : {}), ...(opts?.referrer ? { referrer: opts.referrer } : {}), ...(opts?.referrerPolicy ? { referrerPolicy: opts.referrerPolicy } : {}), ...(opts?.integrity ? { integrity: opts.integrity } : {}), ...(opts?.keepalive ? { keepalive: opts.keepalive } : {}), ...(opts?.signal ? { signal: opts.signal } : {}), }) /** [PLUGINS - AFTERFETCH] */ .then(async (response) => { for (const plugin of plugins) { try { const afterFetchResult = await plugin?.afterFetch?.({ responseType, headers, fetchFn, baseURL, url, method, opts, data, response, }); if (afterFetchResult) { response = afterFetchResult; } } catch (e) { log(`Plugin "${plugin.name}" error on afterFetch hook`); throw e; } } return response; }) /** [STATUS] */ .then(handleStatus) /** [RESPONSETYPE] */ .then((response) => response[responseType]()) .then(async (data) => { for (const plugin of plugins) { try { data = (await plugin?.transform?.(data)) ?? data; } catch (e) { log(`Plugin "${plugin.name}" error on transform hook`); throw e; } } log(`Fetch successful ${method} ${url}`, data); return data; }) /** [PLUGINS - HANDLEERROR] */ .catch(async (e) => { log(`Fetch failed ${method} ${url}`, e); const shouldThrow = plugins.length === 0 || ( await Promise.all( plugins.map(({ handleError }) => handleError?.(e) ?? true), ) ).every((_) => !!_); if (shouldThrow) throw e; }) ); } /** @type {import('./types.js').BodylessMethod} */ get = (url, opts) => this.fetch(url, "GET", opts); /** @type {import('./types.js').BodylessMethod} */ options = (url, opts) => this.fetch(url, "OPTIONS", opts); /** @type {import('./types.js').BodylessMethod} */ delete = (url, opts) => this.fetch(url, "DELETE", opts); /** @type {import('./types.js').BodylessMethod} */ head = (url, opts) => this.fetch(url, "HEAD", opts); /** @type {import('./types.js').BodyMethod} */ post = (url, data, opts) => this.fetch(url, "POST", opts, data); /** @type {import('./types.js').BodyMethod} */ put = (url, data, opts) => this.fetch(url, "PUT", opts, data); /** @type {import('./types.js').BodyMethod} */ patch = (url, data, opts) => this.fetch(url, "PATCH", opts, data); } export const api = new Api();