mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
JS: Converted http service to ts
This commit is contained in:
parent
634b0aaa07
commit
fb87fb5750
@ -1,5 +1,5 @@
|
|||||||
import {EventManager} from './services/events.ts';
|
import {EventManager} from './services/events.ts';
|
||||||
import * as httpInstance from './services/http';
|
import {HttpManager} from './services/http.ts';
|
||||||
import Translations from './services/translations';
|
import Translations from './services/translations';
|
||||||
import * as componentMap from './components';
|
import * as componentMap from './components';
|
||||||
import {ComponentStore} from './services/components.ts';
|
import {ComponentStore} from './services/components.ts';
|
||||||
@ -20,7 +20,7 @@ window.importVersioned = function importVersioned(moduleName) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Set events and http services on window
|
// Set events and http services on window
|
||||||
window.$http = httpInstance;
|
window.$http = new HttpManager();
|
||||||
window.$events = new EventManager();
|
window.$events = new EventManager();
|
||||||
|
|
||||||
// Translation setup
|
// Translation setup
|
||||||
|
3
resources/js/global.d.ts
vendored
3
resources/js/global.d.ts
vendored
@ -1,9 +1,12 @@
|
|||||||
import {ComponentStore} from "./services/components";
|
import {ComponentStore} from "./services/components";
|
||||||
import {EventManager} from "./services/events";
|
import {EventManager} from "./services/events";
|
||||||
|
import {HttpManager} from "./services/http";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
$components: ComponentStore,
|
$components: ComponentStore,
|
||||||
$events: EventManager,
|
$events: EventManager,
|
||||||
|
$http: HttpManager,
|
||||||
|
baseUrl: (path: string) => string;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Docs: https://www.diagrams.net/doc/faq/embed-mode
|
// Docs: https://www.diagrams.net/doc/faq/embed-mode
|
||||||
import * as store from './store';
|
import * as store from './store';
|
||||||
import {ConfirmDialog} from "../components";
|
import {ConfirmDialog} from "../components";
|
||||||
|
import {HttpError} from "./http";
|
||||||
|
|
||||||
type DrawioExportEventResponse = {
|
type DrawioExportEventResponse = {
|
||||||
action: 'export',
|
action: 'export',
|
||||||
@ -145,9 +146,10 @@ export function close() {
|
|||||||
export async function load(drawingId: string): Promise<string> {
|
export async function load(drawingId: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof window.$http.HttpError) {
|
if (error instanceof HttpError) {
|
||||||
window.$events.showResponseError(error);
|
window.$events.showResponseError(error);
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import {HttpError} from "./http";
|
||||||
|
|
||||||
export class EventManager {
|
export class EventManager {
|
||||||
protected listeners: Record<string, ((data: {}) => void)[]> = {};
|
protected listeners: Record<string, ((data: {}) => void)[]> = {};
|
||||||
protected stack: {name: string, data: {}}[] = [];
|
protected stack: {name: string, data: {}}[] = [];
|
||||||
@ -62,9 +64,9 @@ export class EventManager {
|
|||||||
/**
|
/**
|
||||||
* Notify standard server-provided error messages.
|
* 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) 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);
|
this.error(responseErr.data.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
221
resources/js/services/http.ts
Normal file
221
resources/js/services/http.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user