import * as querystring from "querystring" ; import safeStringify from "json-stringify-safe" ; import { AxiosError , AxiosInstance , AxiosRequestConfig , default as Axios } from "axios" ; import { v4 as uuid } from "uuid" ; import { debug } from "./debug" ; import { createFluidServiceNetworkError , INetworkErrorDetails } from "./error" ; export abstract class RestWrapper { constructor ( protected readonly baseurl ? , protected defaultQueryString = { } , protected readonly maxBodyLength = 1000 * 1024 * 1024 , protected readonly maxContentLength = 1000 * 1024 * 1024 ) { } public async get < T > ( url , queryString ? , headers ? ) { const options = { baseURL : this . baseurl , headers , maxBodyLength : this . maxBodyLength , maxContentLength : this . maxContentLength , method : "GET" , url : `${ url }${ this . generateQueryString ( queryString ) }` } ; return this . request < T > ( options , 200 ) ; } public async post < T > ( url , requestBody , queryString ? , headers ? ) { const options = { baseURL : this . baseurl , data : requestBody , headers , maxBodyLength : this . maxBodyLength , maxContentLength : this . maxContentLength , method : "POST" , url : `${ url }${ this . generateQueryString ( queryString ) }` } ; return this . request < T > ( options , 201 ) ; } public async delete < T > ( url , queryString ? , headers ? ) { const options = { baseURL : this . baseurl , headers , maxBodyLength : this . maxBodyLength , maxContentLength : this . maxContentLength , method : "DELETE" , url : `${ url }${ this . generateQueryString ( queryString ) }` } ; return this . request < T > ( options , 204 ) ; } public async patch < T > ( url , requestBody , queryString ? , headers ? ) { const options = { baseURL : this . baseurl , data : requestBody , headers , maxBodyLength : this . maxBodyLength , maxContentLength : this . maxContentLength , method : "PATCH" , url : `${ url }${ this . generateQueryString ( queryString ) }` } ; return this . request < T > ( options , 200 ) ; } protected abstract request < T > ( options , statusCode ) ; protected generateQueryString ( queryStringValues ) { if ( this . defaultQueryString || queryStringValues ) { const queryStringMap = { ... this . defaultQueryString , ... queryStringValues } ; const queryString = querystring . stringify ( queryStringMap ) ; if ( queryString !== "" ) { return `?${ queryString }` ; } } return "" ; } } export class BasicRestWrapper extends RestWrapper { constructor ( baseurl ? , defaultQueryString = { } , maxBodyLength = 1000 * 1024 * 1024 , maxContentLength = 1000 * 1024 * 1024 , private defaultHeaders = { } , private readonly axios = Axios , private readonly refreshDefaultQueryString ? , private readonly refreshDefaultHeaders ? , private readonly getCorrelationId ? ) { super ( baseurl , defaultQueryString , maxBodyLength , maxContentLength ) ; } protected async request < T > ( requestConfig , statusCode , canRetry = true ) { const options = { ... requestConfig } ; options . headers = this . generateHeaders ( options . headers , ( this . getCorrelationId && this . getCorrelationId ( ) ) || uuid ( ) ) ; return new Promise < T > ( ( resolve , reject ) => { this . axios . request < T > ( options ) . then ( ( response ) => { resolve ( response . data ) ; } ) . catch ( ( error ) => { if ( error ?. response ?. status === statusCode ) { resolve ( error ?. response ?. data ) ; } if ( error && error . config ) { debug ( `[${ error . config . method }] request to [${ error . config . baseURL ?? "" }${ error . config . url ?? "" }] failed with [${ error . response ?. status }] [${ safeStringify ( error . response ?. data , undefined , 2 ) }]` ) ; } else { debug ( `request to ${ options . url } failed ${ error ? error . message : "" }` ) ; } if ( error ?. response ?. status === 429 && error ?. response ?. data ?. retryAfter > 0 && canRetry ) { setTimeout ( ( ) => { this . request < T > ( options , statusCode ) . then ( resolve ) . catch ( reject ) ; } , error . response . data . retryAfter * 1000 ) ; } else if ( error ?. response ?. status === 401 && canRetry && this . refreshOnAuthError ( ) ) { const retryConfig = { ... requestConfig } ; retryConfig . headers = this . generateHeaders ( retryConfig . headers , options . headers [ "x-correlation-id" ] ) ; this . request < T > ( retryConfig , statusCode , false ) . then ( resolve ) . catch ( reject ) ; } else { if ( error ?. response ) { reject ( createFluidServiceNetworkError ( error ?. response ?. status , error ?. response ?. data ) ) ; } else if ( error ?. request ) { reject ( createFluidServiceNetworkError ( 502 , `Network Error: ${ error ?. message ?? "undefined" }` ) ) ; } else { const details = { canRetry : false , isFatal : false , message : error ?. message ?? "Unknown Error" } ; reject ( createFluidServiceNetworkError ( 500 , details ) ) ; } } } ) ; } ) ; } private generateHeaders ( headers ? , fallbackCorrelationId ? ) { let result = headers ?? { } ; if ( this . defaultHeaders ) { result = { ... this . defaultHeaders , ... headers } ; } if ( result [ "x-correlation-id" ] ) { return result ; } return { "x-correlation-id" : fallbackCorrelationId , ... result } ; } private refreshOnAuthError ( ) { if ( this . refreshDefaultQueryString === undefined && this . refreshDefaultHeaders === undefined ) { return false ; } if ( this . refreshDefaultHeaders !== undefined ) { this . defaultHeaders = this . refreshDefaultHeaders ( ) ; } if ( this . refreshDefaultQueryString !== undefined ) { this . defaultQueryString = this . refreshDefaultQueryString ( ) ; } return true ; } }