@soleil-se/app-util
Version:
Utility functions for WebApps, RESTApps and Widgets in Sitevision.
163 lines (148 loc) • 5.46 kB
JavaScript
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 }));
});
}