From 1fe3cc35230c04c40a8e1e9df239c152805a5cae Mon Sep 17 00:00:00 2001 From: Edouard DUPIN Date: Fri, 31 May 2024 19:52:00 +0200 Subject: [PATCH] [FIX] Correct the fail retur API to transmit the error from backend when compatible and wrat it when error occured --- src/resources/rest-tools.ts | 687 +++++++++++++++++++----------------- 1 file changed, 370 insertions(+), 317 deletions(-) diff --git a/src/resources/rest-tools.ts b/src/resources/rest-tools.ts index 941c83e..77ea38a 100644 --- a/src/resources/rest-tools.ts +++ b/src/resources/rest-tools.ts @@ -4,389 +4,442 @@ * @license MPL-2 */ -import { RestErrorResponse } from "./model"; +import { RestErrorResponse, isRestErrorResponse } from "./model"; export enum HTTPRequestModel { - DELETE = "DELETE", - GET = "GET", - PATCH = "PATCH", - POST = "POST", - PUT = "PUT", + DELETE = "DELETE", + GET = "GET", + PATCH = "PATCH", + POST = "POST", + PUT = "PUT", } export enum HTTPMimeType { - ALL = "*/*", - CSV = "text/csv", - IMAGE = "image/*", - IMAGE_JPEG = "image/jpeg", - IMAGE_PNG = "image/png", - JSON = "application/json", - MULTIPART = "multipart/form-data", - OCTET_STREAM = "application/octet-stream", - TEXT_PLAIN = "text/plain", + ALL = "*/*", + CSV = "text/csv", + IMAGE = "image/*", + IMAGE_JPEG = "image/jpeg", + IMAGE_PNG = "image/png", + JSON = "application/json", + MULTIPART = "multipart/form-data", + OCTET_STREAM = "application/octet-stream", + TEXT_PLAIN = "text/plain", } export interface RESTConfig { - // base of the server: http(s)://my.server.org/plop/api/ - server: string; - // Token to access of the data. - token?: string; + // base of the server: http(s)://my.server.org/plop/api/ + server: string; + // Token to access of the data. + token?: string; } export interface RESTModel { - // base of the local API request: "sheep/{id}". - endPoint: string; - // Type of the request. - requestType?: HTTPRequestModel; - // Input type requested. - accept?: HTTPMimeType; - // Content of the local data. - contentType?: HTTPMimeType; - // Mode of the TOKEN in URL or Header (?token:${tokenInUrl}) - tokenInUrl?: boolean; + // base of the local API request: "sheep/{id}". + endPoint: string; + // Type of the request. + requestType?: HTTPRequestModel; + // Input type requested. + accept?: HTTPMimeType; + // Content of the local data. + contentType?: HTTPMimeType; + // Mode of the TOKEN in URL or Header (?token:${tokenInUrl}) + tokenInUrl?: boolean; } export interface ModelResponseHttp { - status: number; - data: any; + status: number; + data: any; } function isNullOrUndefined(data: any): data is undefined | null { - return data === undefined || data === null; + return data === undefined || data === null; } // generic progression callback export type ProgressCallback = (count: number, total: number) => void; export interface RESTAbort { - abort?: () => boolean; + abort?: () => boolean; } // Rest generic callback have a basic model to upload and download advancement. export interface RESTCallbacks { - progressUpload?: ProgressCallback; - progressDownload?: ProgressCallback; - abortHandle?: RESTAbort; + progressUpload?: ProgressCallback; + progressDownload?: ProgressCallback; + abortHandle?: RESTAbort; } export interface RESTRequestType { - restModel: RESTModel; - restConfig: RESTConfig; - data?: any; - params?: object; - queries?: object; - callback?: RESTCallbacks; + restModel: RESTModel; + restConfig: RESTConfig; + data?: any; + params?: object; + queries?: object; + callback?: RESTCallbacks; } function replaceAll(input, searchValue, replaceValue) { - return input.split(searchValue).join(replaceValue); + return input.split(searchValue).join(replaceValue); } function removeTrailingSlashes(input: string): string { - if (isNullOrUndefined(input)) { - return "undefined"; - } - return input.replace(/\/+$/, ""); + if (isNullOrUndefined(input)) { + return "undefined"; + } + return input.replace(/\/+$/, ""); } function removeLeadingSlashes(input: string): string { - if (isNullOrUndefined(input)) { - return ""; - } - return input.replace(/^\/+/, ""); + if (isNullOrUndefined(input)) { + return ""; + } + return input.replace(/^\/+/, ""); } export function RESTUrl({ - restModel, - restConfig, - params, - queries, + restModel, + restConfig, + params, + queries, }: RESTRequestType): string { - // Create the URL PATH: - let generateUrl = `${removeTrailingSlashes( - restConfig.server - )}/${removeLeadingSlashes(restModel.endPoint)}`; - if (params !== undefined) { - for (let key of Object.keys(params)) { - generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`); + // Create the URL PATH: + let generateUrl = `${removeTrailingSlashes( + restConfig.server + )}/${removeLeadingSlashes(restModel.endPoint)}`; + if (params !== undefined) { + for (let key of Object.keys(params)) { + generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`); + } + } + if ( + queries === undefined && + (restConfig.token === undefined || restModel.tokenInUrl !== true) + ) { + return generateUrl; + } + const searchParams = new URLSearchParams(); + if (queries !== undefined) { + for (let key of Object.keys(queries)) { + const value = queries[key]; + if (Array.isArray(value)) { + for (const element of value) { + searchParams.append(`${key}`, `${element}`); } + } else { + searchParams.append(`${key}`, `${value}`); + } } - if ( - queries === undefined && - (restConfig.token === undefined || restModel.tokenInUrl !== true) - ) { - return generateUrl; - } - const searchParams = new URLSearchParams(); - if (queries !== undefined) { - for (let key of Object.keys(queries)) { - const value = queries[key]; - if (Array.isArray(value)) { - for (const element of value) { - searchParams.append(`${key}`, `${element}`); - } - } else { - searchParams.append(`${key}`, `${value}`); - } - } - } - if (restConfig.token !== undefined && restModel.tokenInUrl === true) { - searchParams.append("Authorization", `Bearer ${restConfig.token}`); - } - return generateUrl + "?" + searchParams.toString(); + } + if (restConfig.token !== undefined && restModel.tokenInUrl === true) { + searchParams.append("Authorization", `Bearer ${restConfig.token}`); + } + return generateUrl + "?" + searchParams.toString(); } export function fetchProgress( - generateUrl: string, - { - method, - headers, - body, - }: { - method: HTTPRequestModel; - headers: any; - body: any; - }, - { progressUpload, progressDownload, abortHandle }: RESTCallbacks + generateUrl: string, + { + method, + headers, + body, + }: { + method: HTTPRequestModel; + headers: any; + body: any; + }, + { progressUpload, progressDownload, abortHandle }: RESTCallbacks ): Promise { - const xhr: { - io?: XMLHttpRequest; - } = { - io: new XMLHttpRequest(), - }; - return new Promise((resolve, reject) => { - // Stream the upload progress - if (progressUpload) { - xhr.io?.upload.addEventListener("progress", (dataEvent) => { - if (dataEvent.lengthComputable) { - progressUpload(dataEvent.loaded, dataEvent.total); - } - }); + const xhr: { + io?: XMLHttpRequest; + } = { + io: new XMLHttpRequest(), + }; + return new Promise((resolve, reject) => { + // Stream the upload progress + if (progressUpload) { + xhr.io?.upload.addEventListener("progress", (dataEvent) => { + if (dataEvent.lengthComputable) { + progressUpload(dataEvent.loaded, dataEvent.total); } - // Stream the download progress - if (progressDownload) { - xhr.io?.addEventListener("progress", (dataEvent) => { - if (dataEvent.lengthComputable) { - progressDownload(dataEvent.loaded, dataEvent.total); - } - }); + }); + } + // Stream the download progress + if (progressDownload) { + xhr.io?.addEventListener("progress", (dataEvent) => { + if (dataEvent.lengthComputable) { + progressDownload(dataEvent.loaded, dataEvent.total); } - if (abortHandle) { - abortHandle.abort = () => { - if (xhr.io) { - console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`); - xhr.io.abort(); - return true; - } - console.log( - `Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}` - ); - return false; - }; + }); + } + if (abortHandle) { + abortHandle.abort = () => { + if (xhr.io) { + console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`); + xhr.io.abort(); + return true; } - // Check if we have an internal Fail: - xhr.io?.addEventListener("error", () => { - xhr.io = undefined; - reject(new TypeError("Failed to fetch")); - }); - - // Capture the end of the stream - xhr.io?.addEventListener("loadend", () => { - if (xhr.io?.readyState !== XMLHttpRequest.DONE) { - return; - } - if (xhr.io?.status === 0) { - //the stream has been aborted - reject(new TypeError("Fetch has been aborted")); - return; - } - // Stream is ended, transform in a generic response: - const response = new Response(xhr.io.response, { - status: xhr.io.status, - statusText: xhr.io.statusText, - }); - const headersArray = replaceAll( - xhr.io.getAllResponseHeaders().trim(), - "\r\n", - "\n" - ).split("\n"); - headersArray.forEach(function (header) { - const firstColonIndex = header.indexOf(":"); - if (firstColonIndex !== -1) { - const key = header.substring(0, firstColonIndex).trim(); - const value = header.substring(firstColonIndex + 1).trim(); - response.headers.set(key, value); - } else { - response.headers.set(header, ""); - } - }); - xhr.io = undefined; - resolve(response); - }); - xhr.io?.open(method, generateUrl, true); - if (!isNullOrUndefined(headers)) { - for (const [key, value] of Object.entries(headers)) { - xhr.io?.setRequestHeader(key, value as string); - } - } - xhr.io?.send(body); + console.log( + `Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}` + ); + return false; + }; + } + // Check if we have an internal Fail: + xhr.io?.addEventListener("error", () => { + xhr.io = undefined; + reject(new TypeError("Failed to fetch")); }); + + // Capture the end of the stream + xhr.io?.addEventListener("loadend", () => { + if (xhr.io?.readyState !== XMLHttpRequest.DONE) { + return; + } + if (xhr.io?.status === 0) { + //the stream has been aborted + reject(new TypeError("Fetch has been aborted")); + return; + } + // Stream is ended, transform in a generic response: + const response = new Response(xhr.io.response, { + status: xhr.io.status, + statusText: xhr.io.statusText, + }); + const headersArray = replaceAll( + xhr.io.getAllResponseHeaders().trim(), + "\r\n", + "\n" + ).split("\n"); + headersArray.forEach(function (header) { + const firstColonIndex = header.indexOf(":"); + if (firstColonIndex !== -1) { + const key = header.substring(0, firstColonIndex).trim(); + const value = header.substring(firstColonIndex + 1).trim(); + response.headers.set(key, value); + } else { + response.headers.set(header, ""); + } + }); + xhr.io = undefined; + resolve(response); + }); + xhr.io?.open(method, generateUrl, true); + if (!isNullOrUndefined(headers)) { + for (const [key, value] of Object.entries(headers)) { + xhr.io?.setRequestHeader(key, value as string); + } + } + xhr.io?.send(body); + }); } export function RESTRequest({ - restModel, - restConfig, - data, - params, - queries, - callback, + restModel, + restConfig, + data, + params, + queries, + callback, }: RESTRequestType): Promise { - // Create the URL PATH: - let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries }); - let headers: any = {}; - if (restConfig.token !== undefined && restModel.tokenInUrl !== true) { - headers["Authorization"] = `Bearer ${restConfig.token}`; + // Create the URL PATH: + let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries }); + let headers: any = {}; + if (restConfig.token !== undefined && restModel.tokenInUrl !== true) { + headers["Authorization"] = `Bearer ${restConfig.token}`; + } + if (restModel.accept !== undefined) { + headers["Accept"] = restModel.accept; + } + if (restModel.requestType !== HTTPRequestModel.GET) { + // if Get we have not a content type, the body is empty + if (restModel.contentType !== HTTPMimeType.MULTIPART) { + // special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****" + headers["Content-Type"] = restModel.contentType; } - if (restModel.accept !== undefined) { - headers["Accept"] = restModel.accept; + } + let body = data; + if (restModel.contentType === HTTPMimeType.JSON) { + body = JSON.stringify(data); + } else if (restModel.contentType === HTTPMimeType.MULTIPART) { + const formData = new FormData(); + for (const name in data) { + formData.append(name, data[name]); } - if (restModel.requestType !== HTTPRequestModel.GET) { - // if Get we have not a content type, the body is empty - if (restModel.contentType !== HTTPMimeType.MULTIPART) { - // special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****" - headers["Content-Type"] = restModel.contentType; - } + body = formData; + } + return new Promise((resolve, reject) => { + let action: undefined | Promise = undefined; + if ( + isNullOrUndefined(callback) || + (isNullOrUndefined(callback.progressDownload) && + isNullOrUndefined(callback.progressUpload) && + isNullOrUndefined(callback.abortHandle)) + ) { + // No information needed: call the generic fetch interface + action = fetch(generateUrl, { + method: restModel.requestType, + headers, + body, + }); + } else { + // need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x + action = fetchProgress( + generateUrl, + { + method: restModel.requestType ?? HTTPRequestModel.GET, + headers, + body, + }, + callback + ); } - let body = data; - if (restModel.contentType === HTTPMimeType.JSON) { - body = JSON.stringify(data); - } else if (restModel.contentType === HTTPMimeType.MULTIPART) { - const formData = new FormData(); - for (const name in data) { - formData.append(name, data[name]); - } - body = formData; - } - return new Promise((resolve, reject) => { - let action: undefined | Promise = undefined; - if ( - isNullOrUndefined(callback) || - (isNullOrUndefined(callback.progressDownload) && - isNullOrUndefined(callback.progressUpload) && - isNullOrUndefined(callback.abortHandle)) - ) { - // No information needed: call the generic fetch interface - action = fetch(generateUrl, { - method: restModel.requestType, - headers, - body, - }); - } else { - // need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x - action = fetchProgress( - generateUrl, - { - method: restModel.requestType ?? HTTPRequestModel.GET, - headers, - body, - }, - callback - ); - } - action - .then((response: Response) => { - if (response.status >= 200 && response.status <= 299) { - const contentType = response.headers.get("Content-Type"); - if ( - !isNullOrUndefined(restModel.accept) && - restModel.accept !== contentType - ) { - reject({ - name: "Model accept type incompatible", - time: Date().toString(), - status: 901, - error: `REST check wrong type: ${restModel.accept} != ${contentType}`, - statusMessage: "Fetch error", - message: "rest-tools.ts Wrong type in the message return type", - } as RestErrorResponse); - } else if (contentType === HTTPMimeType.JSON) { - response - .json() - .then((value: any) => { - resolve({ status: response.status, data: value }); - }) - .catch((reason: any) => { - reject({ - name: "API serialization error", - time: Date().toString(), - status: 902, - error: `REST parse json fail: ${reason}`, - statusMessage: "Fetch parse error", - message: "rest-tools.ts Wrong message model to parse", - } as RestErrorResponse); - }); - } else { - resolve({ status: response.status, data: response.body }); - } - } else { - reject({ - name: "REST return no OK status", - time: Date().toString(), - status: response.status, - error: `${response.body}`, - statusMessage: "Fetch code error", - message: "rest-tools.ts Wrong return code", - } as RestErrorResponse); - } - }) - .catch((error: any) => { + action + .then((response: Response) => { + if (response.status >= 200 && response.status <= 299) { + const contentType = response.headers.get("Content-Type"); + if ( + !isNullOrUndefined(restModel.accept) && + restModel.accept !== contentType + ) { + reject({ + name: "Model accept type incompatible", + time: Date().toString(), + status: 901, + message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`, + statusMessage: "Fetch error", + error: "rest-tools.ts Wrong type in the message return type", + } as RestErrorResponse); + } else if (contentType === HTTPMimeType.JSON) { + response + .json() + .then((value: any) => { + resolve({ status: response.status, data: value }); + }) + .catch((reason: Error) => { reject({ - name: "Request fail", - time: Date(), - status: 999, - error: error, - statusMessage: "Fetch catch error", - message: "rest-tools.ts detect an error in the fetch request", + name: "API serialization error", + time: Date().toString(), + status: 902, + message: `REST parse json fail: ${reason}`, + statusMessage: "Fetch parse error", + error: "rest-tools.ts Wrong message model to parse", + } as RestErrorResponse); + }); + } else { + resolve({ status: response.status, data: response.body }); + } + } else { + // the answer is not correct not a 2XX + // clone the response to keep the raw data if case of error: + response + .clone() + .json() + .then((value: any) => { + if (isRestErrorResponse(value)) { + reject(value); + } else { + response + .text() + .then((dataError: string) => { + reject({ + name: "API serialization error", + time: Date().toString(), + status: 903, + message: `REST parse error json with wrong type fail. ${dataError}`, + statusMessage: "Fetch parse error", + error: "rest-tools.ts Wrong message model to parse", + } as RestErrorResponse); + }) + .catch((reason: any) => { + reject({ + name: "API serialization error", + time: Date().toString(), + status: response.status, + message: `unmanaged error model: ??? with error: ${reason}`, + statusMessage: "Fetch ERROR parse error", + error: "rest-tools.ts Wrong message model to parse", + } as RestErrorResponse); + }); + } + }) + .catch((reason: Error) => { + response + .text() + .then((dataError: string) => { + reject({ + name: "API serialization error", + time: Date().toString(), + status: response.status, + message: `unmanaged error model: ${dataError} with error: ${reason}`, + statusMessage: "Fetch ERROR TEXT parse error", + error: "rest-tools.ts Wrong message model to parse", + } as RestErrorResponse); + }) + .catch((reason: any) => { + reject({ + name: "API serialization error", + time: Date().toString(), + status: response.status, + message: `unmanaged error model: ??? with error: ${reason}`, + statusMessage: "Fetch ERROR TEXT FAIL", + error: "rest-tools.ts Wrong message model to parse", + } as RestErrorResponse); }); }); - }); + } + }) + .catch((error: Error) => { + if (isRestErrorResponse(error)) { + reject(error); + } else { + reject({ + name: "Request fail", + time: Date(), + status: 999, + message: error, + statusMessage: "Fetch catch error", + error: "rest-tools.ts detect an error in the fetch request", + }); + } + }); + }); } export function RESTRequestJson( - request: RESTRequestType, - checker?: (data: any) => data is TYPE + request: RESTRequestType, + checker?: (data: any) => data is TYPE ): Promise { - return new Promise((resolve, reject) => { - RESTRequest(request) - .then((value: ModelResponseHttp) => { - if (isNullOrUndefined(checker)) { - console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`); - resolve(value.data); - } else if (checker === undefined || checker(value.data)) { - resolve(value.data); - } else { - reject({ - name: "Model check fail", - time: Date().toString(), - status: 950, - error: "REST Fail to verify the data", - statusMessage: "API cast ERROR", - message: "api.ts Check type as fail", - } as RestErrorResponse); - } - }) - .catch((reason: RestErrorResponse) => { - reject(reason); - }); - }); + return new Promise((resolve, reject) => { + RESTRequest(request) + .then((value: ModelResponseHttp) => { + if (isNullOrUndefined(checker)) { + console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`); + resolve(value.data); + } else if (checker === undefined || checker(value.data)) { + resolve(value.data); + } else { + reject({ + name: "Model check fail", + time: Date().toString(), + status: 950, + error: "REST Fail to verify the data", + statusMessage: "API cast ERROR", + message: "api.ts Check type as fail", + } as RestErrorResponse); + } + }) + .catch((reason: RestErrorResponse) => { + reject(reason); + }); + }); } export function RESTRequestVoid(request: RESTRequestType): Promise { - return new Promise((resolve, reject) => { - RESTRequest(request) - .then((value: ModelResponseHttp) => { - resolve(); - }) - .catch((reason: RestErrorResponse) => { - reject(reason); - }); - }); + return new Promise((resolve, reject) => { + RESTRequest(request) + .then((value: ModelResponseHttp) => { + resolve(); + }) + .catch((reason: RestErrorResponse) => { + reject(reason); + }); + }); }