UNPKG

grab-api.js

Version:

Generate Request to API from Browser

475 lines (474 loc) 18.9 kB
function log(message = "", options = {}) { let { color = null, style = "color: blue; font-size: 12pt;", hideInProduction = void 0, startSpinner = false, stopSpinner = false } = options; if (typeof hideInProduction === "undefined") hideInProduction = typeof window !== "undefined" && (window == null ? void 0 : window.location.hostname.includes("localhost")); if (typeof message === "object") message = printStructureJSON(message) + "\n\n" + JSON.stringify(message, null, 2); if (style.split(" ").length == 1 || color) { style = `color: ${style || color}; font-size: 12pt;`; } else { if (style.match(/^#[0-9a-fA-F]{6}$/)) { style = `color: ${style}; font-size: 12pt;`; } } var i = 0; if (startSpinner) (global || globalThis).interval = setInterval(() => { process.stdout.write( "\r" + "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".split("")[i = ++i % 10] + " " + message ); }, 50); else if (stopSpinner) { clearInterval((global || globalThis).interval); process.stdout.write("\r" + message + " ".repeat(message.length) + "\n"); } else if (typeof style === "string") { if (hideInProduction) console.debug((style ? "%c" : "") + message, style); else console.log((style ? "%c" : "") + message, style); } else if (typeof style === "object") console.log(message, ...style); return true; } const colors = { reset: "\x1B[0m", // Reset to default color yellow: "\x1B[33m", // Used for strings cyan: "\x1B[36m", // Used for numbers magenta: "\x1B[35m", // Used for booleans gray: "\x1B[90m", // Used for null values green: "\x1B[32m", // Used for object braces blue: "\x1B[34m", // Used for array brackets red: "\x1B[31m", // Used for functions white: "\x1B[37m" // Default color }; function getColorForType(value) { if (typeof value === "string") return colors.yellow; if (typeof value === "number") return colors.cyan; if (typeof value === "boolean") return colors.magenta; if (typeof value === "function") return colors.red; if (value === null) return colors.gray; if (Array.isArray(value)) return colors.blue; if (typeof value === "object") return colors.green; return colors.white; } function getTypeString(value) { if (typeof value === "string") return '""'; if (typeof value === "number") return "number"; if (typeof value === "boolean") return "bool"; if (typeof value === "function") return "function"; if (value === null) return "null"; if (Array.isArray(value)) { if (value.length) return "[" + getTypeString(value[0]) + "]"; else return "[]"; } if (typeof value === "object") return "{...}"; return typeof value; } function printStructureJSON(obj, indent = 0) { const pad = " ".repeat(indent); if (typeof obj !== "object" || obj === null) { const color = getColorForType(obj); return color + getTypeString(obj) + colors.reset; } if (Array.isArray(obj)) { let result2 = colors.blue + "[" + colors.reset; if (obj.length) result2 += "\n"; obj.forEach((item, idx) => { result2 += pad + " " + printStructureJSON(item, indent + 1); if (idx < obj.length - 1) result2 += ","; result2 += "\n"; }); result2 += pad + colors.blue + "]" + colors.reset; return result2; } let result = colors.green + "{" + colors.reset; const keys = Object.keys(obj); if (keys.length) result += "\n"; keys.forEach((key, index) => { const value = obj[key]; const color = getColorForType(value); result += pad + " "; if (typeof value === "object" && value !== null && !Array.isArray(value)) { result += color + key + colors.reset + ": " + printStructureJSON(value, indent + 1); } else if (Array.isArray(value)) { result += color + key + colors.reset + ": " + printStructureJSON(value, indent + 1); } else { result += color + key + ": " + getTypeString(value) + colors.reset; } if (index < keys.length - 1) result += ","; result += "\n"; }); result += pad + colors.green + "}" + colors.reset; return result; } function showAlert(msg) { if (typeof document === "undefined") return; let o = document.getElementById("alert-overlay"), list; if (!o) { o = document.body.appendChild(document.createElement("div")); o.id = "alert-overlay"; o.setAttribute( "style", "position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center" ); o.innerHTML = `<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;"> <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button> <div id="alert-list" style="overflow:auto;flex:1;"></div> </div>`; o.addEventListener("click", (e) => e.target == o && o.remove()); document.getElementById("close-alert").onclick = () => o.remove(); } list = o.querySelector("#alert-list"); list.innerHTML += `<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${msg}</div>`; } function setupDevTools() { document.addEventListener("keydown", (e) => { if (e.key === "i" && e.ctrlKey) { let html = " "; for (let request of grab.log) { html += `<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;"> <b>Path:</b> ${request.path}<br> <b>Request:</b> ${request.request}<br> <b>Response:</b> ${JSON.stringify(request.response, null, 2)}<br> <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()} </div>`; } showAlert(html); } }); } async function grab$1(path, options) { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l; let { headers, response = {}, // Pre-initialized object to set the response in. isLoading and error are also set on this object. method = options.post ? "POST" : options.put ? "PUT" : options.patch ? "PATCH" : "GET", cache = false, // Enable/disable frontend caching cacheForTime = 60, // Seconds to consider data stale and invalidate cache timeout = 30, // Request timeout in seconds baseURL = typeof process !== "undefined" && process.env.SERVER_API_URL || "/api/", // Use env var or default to /api/ cancelOngoingIfNew = false, // Cancel previous request for same path cancelNewIfOngoing = false, // Don't make new request if one is ongoing rateLimit = 0, // Minimum seconds between requests debug = false, // Auto-enable debug on localhost // typeof window !== "undefined" && window?.location?.hostname?.includes("localhost"), infiniteScroll = null, // page key, response field to concatenate, element with results setDefaults = false, // Set these options as defaults for future requests retryAttempts = 0, // Retry failed requests once logger = log, // Custom logger to override the built-in color JSON log() onRequest = null, // Hook to modify request data before request is made onResponse = null, // Hook to modify request data after request is made onError = null, // Hook to modify request data after request is made onStream = null, // Hook to process the response as a stream (i.e., for instant unarchiving) repeatEvery = null, // Repeat request every seconds repeat = 0, // Repeat request this many times debounce = 0, // Seconds to debounce request, wait to execute so that other requests may override regrabOnStale = false, // Refetch when cache is past cacheForTime regrabOnFocus = false, // Refetch on window refocus regrabOnNetwork = false, // Refetch on network change post = false, put = false, patch = false, body = null, ...params // All other params become request params/query } = { // Destructure options with defaults, merging with any globally set defaults ...typeof window !== "undefined" ? (_a = window == null ? void 0 : window.grab) == null ? void 0 : _a.defaults : ((_c = (_b = global || globalThis) == null ? void 0 : _b.grab) == null ? void 0 : _c.defaults) || {}, ...options }; if (path.startsWith("http:") || path.startsWith("https:")) baseURL = ""; else if (!path.startsWith("/") && !path.startsWith("http:") && !baseURL.endsWith("/")) path = "/" + path; try { if (debounce > 0) return await debouncer(async () => { await grab$1(path, { ...options, debounce: 0 }); }, debounce * 1e3); if (repeat > 1) { for (let i = 0; i < repeat; i++) { await grab$1(path, { ...options, repeat: 0 }); } return response; } if (repeatEvery) { setInterval(async () => { await grab$1(path, { ...options, repeat: 0, repeatEvery: null }); }, repeatEvery * 1e3); return response; } if (options == null ? void 0 : options.setDefaults) { if (typeof window !== "undefined") window.grab.defaults = { ...options, setDefaults: void 0 }; else if (typeof (global || globalThis).grab !== "undefined") (global || globalThis).grab.defaults = { ...options, setDefaults: void 0 }; return; } if (typeof window !== void 0) { const regrab = async () => await grab$1(path, { ...options, cache: false }); if (regrabOnStale && cache) setTimeout(regrab, 1e3 * cacheForTime); if (regrabOnNetwork) window.addEventListener("online", regrab); if (regrabOnFocus) { window.addEventListener("focus", regrab); document.addEventListener("visibilitychange", async () => { if (document.visibilityState === "visible") await regrab(); }); } } let resFunction = typeof response === "function" ? response : null; if (!response || resFunction) response = {}; var [paginateKey, paginateResult, paginateElement] = infiniteScroll || []; if ((infiniteScroll == null ? void 0 : infiniteScroll.length) && typeof window == "undefined") { let paginateDOM = typeof paginateElement === "string" ? document.querySelector(paginateElement) : paginateElement; if (paginateDOM) paginateDOM.removeEventListener("scroll", (_d = window ?? globalThis) == null ? void 0 : _d.scrollListener); (window ?? globalThis).scrollListener = paginateDOM.addEventListener( "scroll", async ({ target }) => { const t = target; localStorage.setItem( "scroll", JSON.stringify([t.scrollTop, t.scrollLeft, paginateElement]) ); if (t.scrollHeight - t.scrollTop <= t.clientHeight + 200) { await grab$1(path, { ...options, cache: false, [paginateKey]: (priorRequest == null ? void 0 : priorRequest.currentPage) + 1 }); } } ); } let paramsAsText = JSON.stringify( paginateKey ? { ...params, [paginateKey]: void 0 } : params ); let priorRequest = (_e = grab$1 == null ? void 0 : grab$1.log) == null ? void 0 : _e.find( (e) => e.request == paramsAsText && e.path == path ); if (!paginateKey) { for (let key of Object.keys(response)) response[key] = void 0; if (cache && (!cacheForTime || (priorRequest == null ? void 0 : priorRequest.lastFetchTime) > Date.now() - 1e3 * cacheForTime)) { for (let key of Object.keys(priorRequest.res)) response[key] = priorRequest.res[key]; if (resFunction) response = resFunction(response); } } else { let pageNumber = (priorRequest == null ? void 0 : priorRequest.currentPage) + 1 || (params == null ? void 0 : params[paginateKey]) || 1; if (!priorRequest) { response[paginateResult] = []; pageNumber = 1; } if (priorRequest) priorRequest.currentPage = pageNumber; params[paginateKey] = pageNumber; } if (resFunction) resFunction({ isLoading: true }); else if (typeof response === "object") response.isLoading = true; if (resFunction) response = resFunction(response); if (rateLimit > 0 && (priorRequest == null ? void 0 : priorRequest.lastFetchTime) && priorRequest.lastFetchTime > Date.now() - 1e3 * rateLimit) throw new Error(`Fetch rate limit exceeded for ${path}. Wait ${rateLimit}s between requests.`); if (priorRequest == null ? void 0 : priorRequest.controller) { if (cancelOngoingIfNew) priorRequest.controller.abort(); else if (cancelNewIfOngoing) return { isLoading: true }; } if (typeof grab$1.log != "undefined") (_f = grab$1.log) == null ? void 0 : _f.unshift({ path, request: paramsAsText, lastFetchTime: Date.now(), controller: new AbortController() }); let fetchParams = { method, headers: { "Content-Type": "application/json", Accept: "application/json", ...headers }, body: params.body, redirect: "follow", cache: cache ? "force-cache" : "no-store", signal: cancelOngoingIfNew ? (_h = (_g = grab$1.log[0]) == null ? void 0 : _g.controller) == null ? void 0 : _h.signal : AbortSignal.timeout(timeout * 1e3) }; let paramsGETRequest = ""; if (["POST", "PUT", "PATCH"].includes(method)) fetchParams.body = params.body || JSON.stringify(params); else paramsGETRequest = (Object.keys(params).length ? "?" : "") + new URLSearchParams(params).toString(); if (typeof onRequest === "function") [path, response, params, fetchParams] = onRequest( path, response, params, fetchParams ); let res = null, startTime = /* @__PURE__ */ new Date(), mockHandler = (_i = grab$1.mock) == null ? void 0 : _i[path]; let wait = (s) => new Promise((res2) => setTimeout(res2, s * 1e3 || 0)); if (mockHandler && (!mockHandler.params || mockHandler.method == method) && (!mockHandler.params || paramsAsText == JSON.stringify(mockHandler.params))) { await wait(mockHandler.delay); res = typeof mockHandler.response === "function" ? mockHandler.response(params) : mockHandler.response; } else { res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch( (e) => { throw new Error(e); } ); if (!res.ok) throw new Error(`HTTP error: ${res.status} ${res.statusText}`); let type = res.headers.get("content-type"); if (onStream) await onStream(res.body); else res = await (type ? type.includes("application/json") ? res && res.json() : type.includes("application/pdf") || type.includes("application/octet-stream") ? res.blob() : res.text() : res.json()).catch((e) => { throw new Error("Error parsing response: " + e); }); } if (typeof onResponse === "function") [path, response, params, fetchParams] = onResponse( path, response, params, fetchParams ); if (resFunction) resFunction({ isLoading: void 0 }); else if (typeof response === "object") response == null ? true : delete response.isLoading; priorRequest == null ? true : delete priorRequest.controller; const elapsedTime = ((Number(/* @__PURE__ */ new Date()) - Number(startTime)) / 1e3).toFixed(1); if (debug) { logger( "Path:" + baseURL + path + paramsGETRequest + "\n" + JSON.stringify(options, null, 2) + "\nTime: " + elapsedTime + "s\nResponse: " + printStructureJSON(res) ); } if (typeof res === "undefined") return; if (typeof res === "object") { for (let key of Object.keys(res)) response[key] = paginateResult == key && ((_j = response[key]) == null ? void 0 : _j.length) ? [...response[key], ...res[key]] : res[key]; if (typeof response !== "undefined") response.data = res; } else if (resFunction) resFunction({ data: res, ...res }); else if (typeof response === "object") response.data = res; if (typeof grab$1.log != "undefined") (_k = grab$1.log) == null ? void 0 : _k.unshift({ path, request: JSON.stringify({ ...params, paginateKey: void 0 }), response, lastFetchTime: Date.now() }); if (resFunction) response = resFunction(response); return response; } catch (error) { let errorMessage = "Error: " + error.message + "\nPath:" + baseURL + path + "\n"; JSON.stringify(params); if (typeof onError === "function") onError( error.message, baseURL + path, params ); if (options.retryAttempts > 0) return await grab$1(path, { ...options, retryAttempts: --options.retryAttempts }); if (!error.message.includes("signal") && options.debug) { logger(errorMessage, "color: red;"); if (debug && typeof document !== void 0) showAlert(errorMessage); } response.error = error.message; if (typeof response === "function") { response.data = response({ isLoading: void 0, error: error.message }); response = response.data; } else response == null ? true : delete response.isLoading; if (typeof grab$1.log != "undefined") (_l = grab$1.log) == null ? void 0 : _l.unshift({ path, request: JSON.stringify(params), error: error.message }); return response; } } grab$1.instance = (defaults = {}) => (path, options = {}) => grab$1(path, { ...defaults, ...options }); const debouncer = async (func, wait) => { let timeout; return async function executedFunction(...args) { const later = async () => { clearTimeout(timeout); await func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; if (typeof window !== "undefined") { window.log = log; window.grab = grab$1; window.grab.log = []; window.grab.mock = {}; window.grab.defaults = {}; setupDevTools(); document.addEventListener("DOMContentLoaded", () => { let [scrollTop, scrollLeft, paginateElement] = JSON.parse(localStorage.getItem("scroll")) || []; if (!scrollTop) return; document.querySelector(paginateElement).scrollTop = scrollTop; document.querySelector(paginateElement).scrollLeft = scrollLeft; }); } else if (typeof global !== "undefined") { grab$1.log = []; grab$1.mock = {}; grab$1.defaults = {}; global.log = log; global.grab = grab$1.instance(); } else if (typeof globalThis !== "undefined") { grab$1.log = []; grab$1.mock = {}; grab$1.defaults = {}; globalThis.log = log; globalThis.grab = grab$1.instance(); } export { grab$1 as default, grab$1 as grab, log, printStructureJSON, showAlert }; //# sourceMappingURL=grab-api.es.js.map