-
/**
- * Perform a HTTP GET request.
- * Can easily pass query parameters as the second parameter.
- * @param {String} url
- * @param {Object} params
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @typedef FormattedResponse
+ * @property {Headers} headers
+ * @property {Response} original
+ * @property {Object|String} data
+ * @property {Boolean} redirected
+ * @property {Number} status
+ * @property {string} statusText
+ * @property {string} url
*/
-async function get(url, params = {}) {
- return request(url, {
- method: 'GET',
- params,
- });
-}
/**
- * Perform a HTTP POST request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * Get the content from a fetch response.
+ * Checks the content-type header to determine the format.
+ * @param {Response} response
+ * @returns {Promise<Object|String>}
*/
-async function post(url, data = null) {
- return dataRequest('POST', url, data);
+async function getResponseContent(response) {
+ if (response.status === 204) {
+ return null;
+ }
+
+ const responseContentType = response.headers.get('Content-Type') || '';
+ const subType = responseContentType.split(';')[0].split('/').pop();
+
+ if (subType === 'javascript' || subType === 'json') {
+ return response.json();
+ }
+
+ return response.text();
}
-/**
- * Perform a HTTP PUT request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function put(url, data = null) {
- return dataRequest('PUT', url, data);
+export class HttpError extends Error {
+
+ constructor(response, content) {
+ super(response.statusText);
+ this.data = content;
+ this.headers = response.headers;
+ this.redirected = response.redirected;
+ this.status = response.status;
+ this.statusText = response.statusText;
+ this.url = response.url;
+ this.original = response;
+ }
+
}
/**
- * Perform a HTTP PATCH request.
+ * @param {String} method
* @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @param {Object} events
+ * @return {XMLHttpRequest}
*/
-async function patch(url, data = null) {
- return dataRequest('PATCH', url, data);
+export function createXMLHttpRequest(method, url, events = {}) {
+ const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
+ const req = new XMLHttpRequest();
+
+ for (const [eventName, callback] of Object.entries(events)) {
+ req.addEventListener(eventName, callback.bind(req));
+ }
+
+ req.open(method, url);
+ req.withCredentials = true;
+ req.setRequestHeader('X-CSRF-TOKEN', csrfToken);
+
+ return req;
}
/**
- * Perform a HTTP DELETE request.
+ * Create a new HTTP request, setting the required CSRF information
+ * to communicate with the back-end. Parses & formats the response.
* @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @param {Object} options
+ * @returns {Promise<FormattedResponse>}
*/
-async function performDelete(url, data = null) {
- return dataRequest('DELETE', url, data);
+async function request(url, options = {}) {
+ let requestUrl = url;
+
+ if (!requestUrl.startsWith('http')) {
+ requestUrl = window.baseUrl(requestUrl);
+ }
+
+ if (options.params) {
+ const urlObj = new URL(requestUrl);
+ for (const paramName of Object.keys(options.params)) {
+ const value = options.params[paramName];
+ if (typeof value !== 'undefined' && value !== null) {
+ urlObj.searchParams.set(paramName, value);
+ }
+ }
+ requestUrl = urlObj.toString();
+ }
+
+ const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
+ const requestOptions = {...options, credentials: 'same-origin'};
+ requestOptions.headers = {
+ ...requestOptions.headers || {},
+ baseURL: window.baseUrl(''),
+ 'X-CSRF-TOKEN': csrfToken,
+ };
+
+ const response = await fetch(requestUrl, requestOptions);
+ const content = await getResponseContent(response);
+ const returnData = {
+ data: content,
+ headers: response.headers,
+ redirected: response.redirected,
+ status: response.status,
+ statusText: response.statusText,
+ url: response.url,
+ original: response,
+ };
+
+ if (!response.ok) {
+ throw new HttpError(response, content);
+ }
+
+ return returnData;
}
/**
* @param {String} method
* @param {String} url
* @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @returns {Promise<FormattedResponse>}
*/
async function dataRequest(method, url, data = null) {
const options = {
- method: method,
+ method,
body: data,
};
options.method = 'post';
}
- return request(url, options)
+ return request(url, options);
}
/**
- * Create a new HTTP request, setting the required CSRF information
- * to communicate with the back-end. Parses & formats the response.
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
* @param {String} url
- * @param {Object} options
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @param {Object} params
+ * @returns {Promise<FormattedResponse>}
*/
-async function request(url, options = {}) {
- if (!url.startsWith('http')) {
- url = window.baseUrl(url);
- }
-
- if (options.params) {
- const urlObj = new URL(url);
- for (let paramName of Object.keys(options.params)) {
- const value = options.params[paramName];
- if (typeof value !== 'undefined' && value !== null) {
- urlObj.searchParams.set(paramName, value);
- }
- }
- url = urlObj.toString();
- }
-
- const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
- options = Object.assign({}, options, {
- 'credentials': 'same-origin',
- });
- options.headers = Object.assign({}, options.headers || {}, {
- 'baseURL': window.baseUrl(''),
- 'X-CSRF-TOKEN': csrfToken,
+export async function get(url, params = {}) {
+ return request(url, {
+ method: 'GET',
+ params,
});
+}
- const response = await fetch(url, options);
- const content = await getResponseContent(response);
- const returnData = {
- data: content,
- headers: response.headers,
- redirected: response.redirected,
- status: response.status,
- statusText: response.statusText,
- url: response.url,
- original: response,
- };
+/**
+ * Perform a HTTP POST request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function post(url, data = null) {
+ return dataRequest('POST', url, data);
+}
- if (!response.ok) {
- throw returnData;
- }
+/**
+ * Perform a HTTP PUT request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function put(url, data = null) {
+ return dataRequest('PUT', url, data);
+}
- return returnData;
+/**
+ * Perform a HTTP PATCH request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function patch(url, data = null) {
+ return dataRequest('PATCH', url, data);
}
/**
- * Get the content from a fetch response.
- * Checks the content-type header to determine the format.
- * @param {Response} response
- * @returns {Promise<Object|String>}
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
*/
-async function getResponseContent(response) {
- if (response.status === 204) {
- return null;
+async function performDelete(url, data = null) {
+ return dataRequest('DELETE', url, data);
+}
+
+export {performDelete as delete};
+
+/**
+ * Parse the response text for an error response to a user
+ * presentable string. Handles a range of errors responses including
+ * validation responses & server response text.
+ * @param {String} text
+ * @returns {String}
+ */
+export function formatErrorResponseText(text) {
+ const data = text.startsWith('{') ? JSON.parse(text) : {message: text};
+ if (!data) {
+ return text;
}
- const responseContentType = response.headers.get('Content-Type');
- const subType = responseContentType.split('/').pop();
+ if (data.message || data.error) {
+ return data.message || data.error;
+ }
- if (subType === 'javascript' || subType === 'json') {
- return await response.json();
+ const values = Object.values(data);
+ const isValidation = values.every(val => {
+ return Array.isArray(val) || val.every(x => typeof x === 'string');
+ });
+
+ if (isValidation) {
+ return values.flat().join(' ');
}
- return await response.text();
+ return text;
}
-
-export default {
- get: get,
- post: post,
- put: put,
- patch: patch,
- delete: performDelete,
-};
\ No newline at end of file