]> BookStack Code Mirror - bookstack/commitdiff
JS: Converted http service to ts
authorDan Brown <redacted>
Thu, 18 Jul 2024 14:13:14 +0000 (15:13 +0100)
committerDan Brown <redacted>
Thu, 18 Jul 2024 14:13:14 +0000 (15:13 +0100)
resources/js/app.js
resources/js/global.d.ts
resources/js/services/drawio.ts
resources/js/services/events.ts
resources/js/services/http.js [deleted file]
resources/js/services/http.ts [new file with mode: 0644]

index 812a451f2333d6206473cff03be3dbefae8e785e..e08b90ba1e46d741b3584e6f7f5f9cf81168440a 100644 (file)
@@ -1,5 +1,5 @@
 import {EventManager} from './services/events.ts';
-import * as httpInstance from './services/http';
+import {HttpManager} from './services/http.ts';
 import Translations from './services/translations';
 import * as componentMap from './components';
 import {ComponentStore} from './services/components.ts';
@@ -20,7 +20,7 @@ window.importVersioned = function importVersioned(moduleName) {
 };
 
 // Set events and http services on window
-window.$http = httpInstance;
+window.$http = new HttpManager();
 window.$events = new EventManager();
 
 // Translation setup
index a9b9275e92cc1a0b8b3d82a8f5298bbbc671276a..1f216b7a53c0238c15783df339bf16cd28deec3c 100644 (file)
@@ -1,9 +1,12 @@
 import {ComponentStore} from "./services/components";
 import {EventManager} from "./services/events";
+import {HttpManager} from "./services/http";
 
 declare global {
     interface Window {
         $components: ComponentStore,
         $events: EventManager,
+        $http: HttpManager,
+        baseUrl: (path: string) => string;
     }
 }
\ No newline at end of file
index 75b161f75d5c2c57150e5c252cc9fce82c5399ae..c0a6b5044bc6256639abd691d5df52d617b1a1b3 100644 (file)
@@ -1,6 +1,7 @@
 // Docs: https://www.diagrams.net/doc/faq/embed-mode
 import * as store from './store';
 import {ConfirmDialog} from "../components";
+import {HttpError} from "./http";
 
 type DrawioExportEventResponse = {
     action: 'export',
@@ -145,9 +146,10 @@ export function close() {
 export async function load(drawingId: string): Promise<string> {
     try {
         const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
-        return `data:image/png;base64,${resp.data.content}`;
+        const data = resp.data as {content: string};
+        return `data:image/png;base64,${data.content}`;
     } catch (error) {
-        if (error instanceof window.$http.HttpError) {
+        if (error instanceof HttpError) {
             window.$events.showResponseError(error);
         }
         close();
index c251ee21b63e923555aaa976dca67b8bfc9550af..7d72a9f1af5f199f0dd5a8d14552ad224cd0b219 100644 (file)
@@ -1,3 +1,5 @@
+import {HttpError} from "./http";
+
 export class EventManager {
     protected listeners: Record<string, ((data: {}) => void)[]> = {};
     protected stack: {name: string, data: {}}[] = [];
@@ -62,9 +64,9 @@ export class EventManager {
     /**
      * Notify standard server-provided error messages.
      */
-    showResponseError(responseErr: {status?: number, data?: {message?: string}}): void {
+    showResponseError(responseErr: {status?: number, data?: Record<any, any>}|HttpError): void {
         if (!responseErr.status) return;
-        if (responseErr.status >= 400 && responseErr.data && responseErr.data.message) {
+        if (responseErr.status >= 400 && typeof responseErr.data === 'object' && responseErr.data.message) {
             this.error(responseErr.data.message);
         }
     }
diff --git a/resources/js/services/http.js b/resources/js/services/http.js
deleted file mode 100644 (file)
index d95e4a5..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-/**
- * @typedef FormattedResponse
- * @property {Headers} headers
- * @property {Response} original
- * @property {Object|String} data
- * @property {Boolean} redirected
- * @property {Number} status
- * @property {string} statusText
- * @property {string} url
- */
-
-/**
- * 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 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();
-}
-
-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;
-    }
-
-}
-
-/**
- * @param {String} method
- * @param {String} url
- * @param {Object} events
- * @return {XMLHttpRequest}
- */
-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;
-}
-
-/**
- * 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} options
- * @returns {Promise<FormattedResponse>}
- */
-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;
-}
-
-/**
- * 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 dataRequest(method, url, data = null) {
-    const options = {
-        method,
-        body: data,
-    };
-
-    // 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);
-    }
-
-    // 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 {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;
-    }
-
-    if (data.message || data.error) {
-        return data.message || data.error;
-    }
-
-    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 text;
-}
diff --git a/resources/js/services/http.ts b/resources/js/services/http.ts
new file mode 100644 (file)
index 0000000..f9eaafc
--- /dev/null
@@ -0,0 +1,221 @@
+type ResponseData = Record<any, any>|string;
+
+type RequestOptions = {
+    params?: Record<string, string>,
+    headers?: Record<string, string>
+};
+
+type FormattedResponse = {
+    headers: Headers;
+    original: Response;
+    data: ResponseData;
+    redirected: boolean;
+    status: number;
+    statusText: string;
+    url: string;
+};
+
+export class HttpError extends Error implements FormattedResponse {
+
+    data: ResponseData;
+    headers: Headers;
+    original: Response;
+    redirected: boolean;
+    status: number;
+    statusText: string;
+    url: string;
+
+    constructor(response: Response, content: ResponseData) {
+        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;
+    }
+}
+
+export class HttpManager {
+
+    /**
+     * Get the content from a fetch response.
+     * Checks the content-type header to determine the format.
+     */
+    protected async getResponseContent(response: Response): Promise<ResponseData|null> {
+        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();
+    }
+
+    createXMLHttpRequest(method: string, url: string, events: Record<string, (e: Event) => void> = {}): XMLHttpRequest {
+        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;
+    }
+
+    /**
+     * Create a new HTTP request, setting the required CSRF information
+     * to communicate with the back-end. Parses & formats the response.
+     */
+    protected async request(url: string, options: RequestOptions & RequestInit = {}): Promise<FormattedResponse> {
+        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: RequestInit = {...options, credentials: 'same-origin'};
+        requestOptions.headers = {
+            ...requestOptions.headers || {},
+            baseURL: window.baseUrl(''),
+            'X-CSRF-TOKEN': csrfToken,
+        };
+
+        const response = await fetch(requestUrl, requestOptions);
+        const content = await this.getResponseContent(response) || '';
+        const returnData: FormattedResponse = {
+            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;
+    }
+
+    /**
+     * 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.
+     */
+    protected async dataRequest(method: string, url: string, data: Record<string, any>|null): Promise<FormattedResponse> {
+        const options: RequestInit & RequestOptions = {
+            method,
+            body: data as BodyInit,
+        };
+
+        // 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);
+        }
+
+        // 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 this.request(url, options);
+    }
+
+    /**
+     * Perform a HTTP GET request.
+     * Can easily pass query parameters as the second parameter.
+     */
+    async get(url: string, params: {} = {}): Promise<FormattedResponse> {
+        return this.request(url, {
+            method: 'GET',
+            params,
+        });
+    }
+
+    /**
+     * Perform a HTTP POST request.
+     */
+    async post(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
+        return this.dataRequest('POST', url, data);
+    }
+
+    /**
+     * Perform a HTTP PUT request.
+     */
+    async put(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
+        return this.dataRequest('PUT', url, data);
+    }
+
+    /**
+     * Perform a HTTP PATCH request.
+     */
+    async patch(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
+        return this.dataRequest('PATCH', url, data);
+    }
+
+    /**
+     * Perform a HTTP DELETE request.
+     */
+    async delete(url: string, data: null|Record<string, any> = null): Promise<FormattedResponse> {
+        return this.dataRequest('DELETE', url, data);
+    }
+
+    /**
+     * 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.
+     */
+    protected formatErrorResponseText(text: string): string {
+        const data = text.startsWith('{') ? JSON.parse(text) : {message: text};
+        if (!data) {
+            return text;
+        }
+
+        if (data.message || data.error) {
+            return data.message || data.error;
+        }
+
+        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 text;
+    }
+
+}