]> BookStack Code Mirror - bookstack/blobdiff - resources/js/services/http.js
Fix timestamp in API docs example response
[bookstack] / resources / js / services / http.js
index 06dac9864a4d4523987e61f4009075ea34ed0594..d0d33e317df9230c2989bbcfefcd0bb2333032cf 100644 (file)
@@ -1,78 +1,48 @@
-
 /**
- * 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;
+    }
 
-/**
- * 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);
-}
+    const responseContentType = response.headers.get('Content-Type') || '';
+    const subType = responseContentType.split(';')[0].split('/').pop();
 
-/**
- * Perform a HTTP PATCH 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 patch(url, data = null) {
-    return dataRequest('PATCH', url, data);
-}
+    if (subType === 'javascript' || subType === 'json') {
+        return response.json();
+    }
 
-/**
- * Perform a HTTP DELETE 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 performDelete(url, data = null) {
-    return dataRequest('DELETE', url, data);
+    return response.text();
 }
 
-/**
- * Perform a HTTP request to the back-end that includes data in the body.
- * Parses the body to JSON if an object, setting the correct headers.
- * @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}>}
- */
-async function dataRequest(method, url, data = null) {
-    const options = {
-        method: method,
-        body: data,
-    };
-
-    if (typeof data === 'object' && !(data instanceof FormData)) {
-        options.headers = {'Content-Type': 'application/json'};
-        options.body = JSON.stringify(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;
     }
 
-    return request(url, options)
 }
 
 /**
@@ -80,36 +50,37 @@ async function dataRequest(method, url, data = null) {
  * to communicate with the back-end. Parses & formats the response.
  * @param {String} url
  * @param {Object} options
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @returns {Promise<FormattedResponse>}
  */
 async function request(url, options = {}) {
-    if (!url.startsWith('http')) {
-        url = window.baseUrl(url);
+    let requestUrl = url;
+
+    if (!requestUrl.startsWith('http')) {
+        requestUrl = window.baseUrl(requestUrl);
     }
 
     if (options.params) {
-        const urlObj = new URL(url);
-        for (let paramName of Object.keys(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);
             }
         }
-        url = urlObj.toString();
+        requestUrl = 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(''),
+    const requestOptions = {...options, credentials: 'same-origin'};
+    requestOptions.headers = {
+        ...requestOptions.headers || {},
+        baseURL: window.baseUrl(''),
         'X-CSRF-TOKEN': csrfToken,
-    });
+    };
 
-    const response = await fetch(url, options);
+    const response = await fetch(requestUrl, requestOptions);
     const content = await getResponseContent(response);
-    return {
+    const returnData = {
         data: content,
         headers: response.headers,
         redirected: response.redirected,
@@ -117,30 +88,101 @@ async function request(url, options = {}) {
         statusText: response.statusText,
         url: response.url,
         original: response,
+    };
+
+    if (!response.ok) {
+        throw new HttpError(response, content);
     }
+
+    return returnData;
 }
 
 /**
- * Get the content from a fetch response.
- * Checks the content-type header to determine the format.
- * @param response
- * @returns {Promise<Object|String>}
+ * Perform a HTTP request to the back-end that includes data in the body.
+ * Parses the body to JSON if an object, setting the correct headers.
+ * @param {String} method
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
  */
-async function getResponseContent(response) {
-    const responseContentType = response.headers.get('Content-Type');
-    const subType = responseContentType.split('/').pop();
+async function dataRequest(method, url, data = null) {
+    const options = {
+        method,
+        body: data,
+    };
 
-    if (subType === 'javascript' || subType === 'json') {
-        return await response.json();
+    // Send data as JSON if a plain object
+    if (typeof data === 'object' && !(data instanceof FormData)) {
+        options.headers = {
+            'Content-Type': 'application/json',
+            'X-Requested-With': 'XMLHttpRequest',
+        };
+        options.body = JSON.stringify(data);
     }
 
-    return await response.text();
+    // Ensure FormData instances are sent over POST
+    // Since Laravel does not read multipart/form-data from other types
+    // of request. Hence the addition of the magic _method value.
+    if (data instanceof FormData && method !== 'post') {
+        data.append('_method', method);
+        options.method = 'post';
+    }
+
+    return request(url, options);
+}
+
+/**
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
+ * @param {String} url
+ * @param {Object} params
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function get(url, params = {}) {
+    return request(url, {
+        method: 'GET',
+        params,
+    });
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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);
+}
+
+/**
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+async function performDelete(url, data = null) {
+    return dataRequest('DELETE', url, data);
 }
 
-export default {
-    get: get,
-    post: post,
-    put: put,
-    patch: patch,
-    delete: performDelete,
-};
\ No newline at end of file
+export {performDelete as delete};