UNPKG

@soleil-se/app-util

Version:

Utility functions for WebApps, RESTApps and Widgets in Sitevision.

163 lines (148 loc) 5.46 kB
import { getRouteUri, stringifyParams } from '../../common'; /** * @typedef {Object} ExtensionOptions * @property {{ [key: string]: unknown }} [params] - The parameters to be included in the request * URL. * @property {number} [retries=0] - The number of retries to attempt in case of a timeout error. * * @typedef {RequestInit & ExtensionOptions} Options */ /** * Custom error class for fetch-related errors with additional HTTP context. */ export class FetchError extends Error { /** * @param {string} message - The error message. * @param {Object} [options] - Error options. * @param {number} [options.status] - HTTP status code. * @param {boolean} [options.aborted=false] - Whether the request was aborted. * @param {Object.<string, unknown>} [options.additionalProps] - Additional properties. */ constructor(message, { status, aborted = false, ...additionalProps } = {}) { super(message); this.name = 'FetchError'; this.status = status; this.aborted = aborted; // Add any additional properties from the error response Object.entries(additionalProps).forEach(([key, value]) => { if (key !== 'message' && key !== 'status' && key !== 'aborted') { this[key] = value; } }); } } /** * Builds the URL for the request, handling different URI formats. * @param {string} uri - The URI to convert to a full URL. * @param {{ [key: string]: unknown }} params - Query parameters to include in the URL. * @returns {string} The complete URL for the request. */ function getUrl(uri, params) { if (uri.startsWith('/rest-api') || uri.startsWith('/appresource') || uri.startsWith('/edit-app-config') || !uri.startsWith('/')) { return uri + stringifyParams(params, { addQueryPrefix: true }); } return getRouteUri(uri, params); } /** * Attempts to parse response text as JSON. * @param {Response} response - The fetch Response object. * @returns {Promise<unknown | undefined>} The parsed JSON object or undefined if parsing fails. */ async function toJson(response) { const text = await response.text(); try { return JSON.parse(text); } catch (e) { return undefined; } } /** * Creates a FetchError object from a failed response, including JSON error details. * @param {Response} response - The failed fetch Response object. * @returns {Promise<FetchError>} A FetchError with additional properties from the response JSON. */ async function responseError(response) { const json = await toJson(response) || {}; const message = json?.message || response?.statusText; return new FetchError(message, { status: response.status, aborted: false, ...json, }); } /** * Handles the fetch response, throwing errors for non-OK responses and parsing JSON. * @param {Response} response - The fetch Response object. * @returns {Promise<unknown>} The parsed JSON response. * @throws {FetchError} If the response is not OK or cannot be parsed as JSON. */ async function handleResponse(response) { if (!response.ok) { throw await responseError(response); } const json = await toJson(response); if (!json) { throw new FetchError('Response could not be parsed as JSON.'); } return json; } /** * Checks if the current context is a SiteVision widget/dashboard. * @returns {boolean} True if running in a widget context. */ function isWidget() { return !!window?.sv?.PageContext?.dashboardId; } /** * Modifies request options for widget context by adding required parameters and headers. * @param {Options} options - The original request options. * @returns {Options} Modified options with widget-specific parameters and headers. */ function getWidgetOptions(options) { return { ...options, params: { svAjaxReqParam: 'ajax', ...options.params, }, headers: { ...options.headers, 'X-Requested-With': 'XMLHttpRequest', }, }; } /** * Fetches JSON data from a specified URI. * URI:s starting with `/rest-api`, `/appresource`, `/edit-app-config` or a protocol, for example * `https://` will be left as is. Other URI:s willbe converted to match a route in the current app * with `getRouteUri`. * * @template T * @param {string} uri - The URI to fetch JSON data from. * @param {Options} [options] - The options for the fetch request with some extensions. * @returns {Promise<T>} A promise that resolves to the JSON response. * @throws {FetchError} If the response is not successful or cannot be parsed as JSON. */ export default function fetchJson(uri, options = {}) { const { params = {}, retries = 0, ...rest } = isWidget() ? getWidgetOptions(options) : options; return fetch(getUrl(uri, params), rest) .then(handleResponse) .catch((error) => { const isTimeout = error.status === 504 || error.status === 408 || error.message?.includes('SocketTimeoutException'); if (isTimeout && retries > 0) { return fetchJson(uri, { ...rest, params, retries: retries - 1 }); } // Handle abort errors if (error.name === 'AbortError') { return Promise.reject(new FetchError(error.message, { status: error.status, aborted: true, })); } // Re-throw FetchError as-is, convert other errors if (error instanceof FetchError) { return Promise.reject(error); } return Promise.reject(new FetchError(error.message, { status: error.status || 500 })); }); }