// Base http-service / json-api class import { stringify } from "querystring"; import { EventEmitter } from "../../common/event-emitter"; import { cancelableFetch } from "../utils/cancelableFetch"; export interface JsonApiData { } export interface JsonApiError { code?: number; message?: string; errors?: { id: string; title: string; status?: number }[]; } export interface JsonApiParams { query?: { [param: string]: string | number | any }; data?: D; // request body } export interface JsonApiLog { method: string; reqUrl: string; reqInit: RequestInit; data?: any; error?: any; } export interface JsonApiConfig { apiBase: string; debug?: boolean; } export class JsonApi { static reqInitDefault: RequestInit = { headers: { "content-type": "application/json" } }; static configDefault: Partial = { debug: false }; constructor(protected config: JsonApiConfig, protected reqInit?: RequestInit) { this.config = Object.assign({}, JsonApi.configDefault, config); this.reqInit = Object.assign({}, JsonApi.reqInitDefault, reqInit); this.parseResponse = this.parseResponse.bind(this); } public onData = new EventEmitter<[D, Response]>(); public onError = new EventEmitter<[JsonApiErrorParsed, Response]>(); get(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "get" }); } getResponse(path: string, params?: P, init: RequestInit = {}): Promise { let reqUrl = `${this.config.apiBase}${path}`; const reqInit: RequestInit = { ...init }; const { query } = params || {} as P; if (!reqInit.method) { reqInit.method = "get"; } if (query) { const queryString = stringify(query); reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; } const infoLog: JsonApiLog = { method: reqInit.method.toUpperCase(), reqUrl, reqInit, }; this.writeLog({ ...infoLog }); return fetch(reqUrl, reqInit); } post(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "post" }); } put(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "put" }); } patch(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "PATCH" }); } del(path: string, params?: P, reqInit: RequestInit = {}) { return this.request(path, params, { ...reqInit, method: "delete" }); } protected request(path: string, params?: P, init: RequestInit = {}) { let reqUrl = this.config.apiBase + path; const reqInit: RequestInit = { ...this.reqInit, ...init }; const { data, query } = params || {} as P; if (data && !reqInit.body) { reqInit.body = JSON.stringify(data); } if (query) { const queryString = stringify(query); reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; } const infoLog: JsonApiLog = { method: reqInit.method.toUpperCase(), reqUrl, reqInit, }; return cancelableFetch(reqUrl, reqInit).then(res => { return this.parseResponse(res, infoLog); }); } protected parseResponse(res: Response, log: JsonApiLog): Promise { const { status } = res; return res.text().then(text => { let data; try { data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body } catch (e) { data = text; } if (status >= 200 && status < 300) { this.onData.emit(data, res); this.writeLog({ ...log, data }); return data; } else if (log.method === "GET" && res.status === 403) { this.writeLog({ ...log, data }); } else { const error = new JsonApiErrorParsed(data, this.parseError(data, res)); this.onError.emit(error, res); this.writeLog({ ...log, error }); throw error; } }); } protected parseError(error: JsonApiError | string, res: Response): string[] { if (typeof error === "string") { return [error]; } else if (Array.isArray(error.errors)) { return error.errors.map(error => error.title); } else if (error.message) { return [error.message]; } return [res.statusText || "Error!"]; } protected writeLog(log: JsonApiLog) { if (!this.config.debug) return; const { method, reqUrl, ...params } = log; let textStyle = "font-weight: bold;"; if (params.data) textStyle += "background: green; color: white;"; if (params.error) textStyle += "background: red; color: white;"; console.log(`%c${method} ${reqUrl}`, textStyle, params); } } export class JsonApiErrorParsed { isUsedForNotification = false; constructor(private error: JsonApiError | DOMException, private messages: string[]) { } get isAborted() { return this.error.code === DOMException.ABORT_ERR; } toString() { return this.messages.join("\n"); } }