@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
JavaScript
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();