import * as Axios from 'axios'; import { DyFM_AnyError, DyFM_Error, DyFM_Error_Settings, DyFM_errorFlag, DyFM_ErrorLevel, DyFM_HttpCallType, DyFM_HttpResponseType, DyFM_Log, DyFM_HttpErrorResponse, DyFM_Object, DyFM_getLocalStackLocation } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_ApiCall_Params } from '../../_models/control-models/api-call-params.control-model'; export interface DyNTS_ApiCallInput_Params { pathParams?: { /** * path params setted in endpoint */ [param: string]: string; }; /** * api call's body */ body?: T_Body; } /** * This predefined Api service contains the basic API call function which can be used in various ways */ export class DyNTS_ApiService { static readonly defaultErrorUserMsg = `We encountered a BackEnd API Error, ` + `\nplease contact the responsible development team.\n` + `\n(Internal BE to BE error)`; /** * if the callParams.getFullResponse is set to true, * use the Axios.AxiosResponse type as T_Response * * @param callParams * @param inputParams * @returns */ public static async startApiCall( /** * you must setup the basic api call params with this. * follow the instructions in the constructor: new DyNTS_ApiCall_Params({ ... }) */ callParams: DyNTS_ApiCall_Params, /** * you can pass data and other inputs in this section */ inputParams?: DyNTS_ApiCallInput_Params ): Promise { let url: string; try { let a: T_Response; url = callParams.baseUrl + callParams.endpoint; const axios = Axios.default.create(); if (!callParams.baseUrl) { DyFM_Log.warn(`WARNING: missing baseUrl in api call (${callParams.name})`); } if (!callParams.endpoint) { DyFM_Log.warn(`WARNING: missing endpoint in api call (${callParams.name})`); } if (callParams.pathParamKeys.length) { callParams.pathParamKeys.forEach(key => { if (!inputParams?.pathParams?.[key]) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'startApiCall', new Error(`missing pathParam: "${key}" (${callParams.name})`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-SAC1`, }); } }); } if (callParams?.httpOptions) { this.setupAxiosConfig(callParams, axios); } if (inputParams?.pathParams) { url = this.setupPathParams(inputParams.pathParams, url, callParams); } /* if (inputParams?.queryParams) { this.setupQueryParams(inputParams, callParams); } */ if (DyNTS_global_settings.log_settings.api_events) { this.logEvent(url, inputParams, callParams); } switch (callParams.type) { case DyFM_HttpCallType.get: if (inputParams?.body) { DyFM_Log.warn(`WARNING you cant send body in get calls (${callParams.name})`); } a = await this.get(axios, url, callParams); break; case DyFM_HttpCallType.delete: if (inputParams?.body) { DyFM_Log.warn('WARNING you cant send body in delete calls'); } a = await this.delete(axios, url, callParams); break; case DyFM_HttpCallType.post: case DyFM_HttpCallType.put: case DyFM_HttpCallType.patch: if (!inputParams?.body) { if (!inputParams) { inputParams = {}; } inputParams.body = {} as T_Body; } a = await this.postPutPatch(axios, callParams, url, inputParams); break; default: throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'startApiCall', new Error(`unknown api call type: ${callParams.type}`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-SAC0`, additionalContent: { callParams, inputParams, }, }); } if ((a as DyFM_Error)?.flag?.includes(DyFM_errorFlag)) { throw a; } return a; /* if (callParams.getFullResponse) { return a as T_Response Axios.AxiosResponse; } else { return a as T_Response; } */ } catch (error) { if ((error as DyFM_Error)?.flag?.includes(DyFM_errorFlag)) { throw error; /* } else if ((error?.response?.data as DyFM_Error)?.flag?.includes(DyFM_errorFlag) && [ DyFM_ErrorLevel.user, DyFM_ErrorLevel.debug, DyFM_ErrorLevel.info, DyFM_ErrorLevel.known, DyFM_ErrorLevel.warning, ].includes((error?.response?.data as DyFM_Error)?.level) ) { return error; */ } else { this.handleError(callParams, error, inputParams, url); } } } /* private static setupQueryParams( inputParams: DyNTS_ApiCallInputParams, callParams: DyNTS_ApiCall_Params) { const httpParams = new HttpParams(); for (const queryParamKey in inputParams.queryParams) { if (inputParams.queryParams[queryParamKey]) { switch (typeof inputParams.queryParams[queryParamKey]) { case 'number': httpParams.set(queryParamKey, inputParams.queryParams[queryParamKey].toString()); break; case 'string': httpParams.set(queryParamKey, inputParams.queryParams[queryParamKey]); break; default: console.error('DYNAMO-NTS ERROR: \n wrong query param type', '\n\n', new Error()); break; } } } callParams.httpOptions.params = httpParams; } */ private static handleError( callParams: DyNTS_ApiCall_Params, error: DyFM_AnyError | Axios.AxiosError, inputParams: { pathParams?: { [param: string]: string; }; /** * api call's body */ body?: T_Body; }, url: string ): void { const errorMsg: string = DyFM_Error.isDyFMError((error as Axios.AxiosError)?.response?.data) ? DyFM_Error.getAnyMessage((error as Axios.AxiosError)?.response?.data as DyFM_AnyError) : DyFM_Error.getAnyMessage(error); let msg: string = `\n---> API ERROR: "${callParams?.name}" failed (${DyFM_Error.getErrorStatus(error)})...` + `\n endpoint: ${callParams?.type} "${callParams?.baseUrl}${callParams?.endpoint}"` + `\n BE-BE requestStackLocation: "${callParams?.stack}"` + `\n error: "${DyFM_Error.getAnyMessage(error)}"` + ((error as DyFM_Error)?.__localStack ? `\n error localStack: "${(error as DyFM_Error)?.__localStack}"` : '') + `\n localStack: "${DyFM_getLocalStackLocation(1)}"`; if (DyNTS_global_settings.log_settings.detailedErrors) { DyFM_Log.S_error(`${msg}\ncallParams:`, callParams); } else { DyFM_Log.S_error(msg); } try { const strError = JSON.stringify(error); if (strError.length < 1000) { msg += `\nerrorObject: ${JSON.stringify(strError, null, 2)}`; } } catch { DyFM_Log.error(`error is not parsable`); } if ( error && typeof (error as DyFM_Error)?.error === 'string' && callParams?.httpOptions?.responseType === DyFM_HttpResponseType.text ) { (error as DyFM_Error).error = DyFM_Object.failableSafeParseJSON( (error as DyFM_Error)?.error as any as string ); } if (DyFM_Error.isDyFMError((error as Axios.AxiosError)?.response?.data)) { DyFM_Log.error(`\n'${callParams.name}' was UNSUCCESSFUL` /* , new Error() */); const dyfmErrorInAxiosResponse = (error as Axios.AxiosError)?.response?.data as DyFM_AnyError; throw new DyFM_Error({ ...this._getDefaultErrorSettings( callParams.name + ' startApiCall', dyfmErrorInAxiosResponse ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE1`, message: `API call "${callParams.name}" failed on the other end!`+ `\nurl: ${url}` + `\nerror response found: ${errorMsg}`, error: dyfmErrorInAxiosResponse, additionalContent: { callParams: callParams, inputParams: inputParams, response: (error as Axios.AxiosError)?.response, }, }); } else if ((error as Axios.AxiosError)?.code === 'ENOTFOUND') { DyFM_Log.error(`\n'${callParams.name}' was UNSUCCESSFUL` /* , new Error() */); throw new DyFM_Error({ ...this._getDefaultErrorSettings( callParams.name + ' startApiCall', error ), status: 404, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE2`, message: 'DNS error; address cannot be resolved!', additionalContent: { callParams: callParams, inputParams: inputParams, response: (error as Axios.AxiosError)?.response, }, }); } else if ((error as Axios.AxiosError)?.code === 'ECONNREFUSED') { DyFM_Log.error(`\n'${callParams.name}' was UNSUCCESSFUL` /* , new Error() */); throw new DyFM_Error({ ...this._getDefaultErrorSettings( callParams.name + ' startApiCall', error ), status: 404, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE3`, message: `Can't connect to the endpoint! \n(${callParams.type} "${url}")`, additionalContent: { callParams: callParams, inputParams: inputParams, url: url, response: (error as Axios.AxiosError)?.response, }, }); } else if ((error as Axios.AxiosError)?.config && (error as Axios.AxiosError)?.message) { let msg: string = (error as Axios.AxiosError)?.message; let status: number = (error as Axios.AxiosError)?.response?.status || (error as Axios.AxiosError)?.status || /* get status from "status code {3 digits}" that might be in the middle of the message */ +(error as Axios.AxiosError)?.message?.match?.(/status code (\d{3})/)?.[1] || 500; if ((error as Axios.AxiosError)?.message?.includes('status code')) { msg += `"${callParams.name}"` + `\nendpoint: ${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"` + `\nurl: ${url}` + `\npathParams: ${ inputParams?.pathParams ? Object.keys(inputParams.pathParams).join(', ') : '-no path params-' }` + `\nenpointStackLocation: ${callParams.stack}`; } throw new DyFM_Error({ ...this._getDefaultErrorSettings( callParams.name + ' startApiCall', error ), status: status, message: msg, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE4`, additionalContent: { callParams: callParams, inputParams: inputParams, response: (error as Axios.AxiosError)?.response, }, }); } else { DyFM_Error.logSimple(`"${callParams.name}" api call was UNSUCCESSFUL`, error); /* if ( DyNTS_global_settings.log_settings.detailedErrors || !(error instanceof DyFM_Error) ) { DyFM_Log.error( `"${callParams.name}" api call was UNSUCCESSFUL`, '\nERROR:', error ); } else { error.logSimple(`"${callParams.name}" api call was UNSUCCESSFUL`); } */ throw error; } } private static async postPutPatch( axios: Axios.AxiosInstance, callParams: DyNTS_ApiCall_Params, url: string, inputParams: DyNTS_ApiCallInput_Params ): Promise { return await axios[callParams.type]( url, inputParams.body, callParams.httpOptions ).then((res: Axios.AxiosResponse): T_Response => { if (DyNTS_global_settings.log_settings.api_events) { if (DyNTS_global_settings.log_settings.api_responseContents) { DyFM_Log.success(`---> "${callParams.name}" api call was successful`, res.data); } else { DyFM_Log.success(`---> "${callParams.name}" api call was successful`); } } if (callParams.getFullResponse) { return res as T_Response; } else { return res.data; } }); } private static async delete( axios: Axios.AxiosInstance, url: string, callParams: DyNTS_ApiCall_Params ): Promise { return await axios.delete(url, callParams.httpOptions).then( (res: Axios.AxiosResponse): T_Response => { if (DyNTS_global_settings.log_settings.api_events) { if (DyNTS_global_settings.log_settings.api_responseContents) { DyFM_Log.success(`"${callParams.name}" api call was successful`, res.data); } else { DyFM_Log.success(`"${callParams.name}" api call was successful`); } } if (callParams.getFullResponse) { return res as T_Response; } else { return res.data; } } ); } private static async get( axios: Axios.AxiosInstance, url: string, callParams: DyNTS_ApiCall_Params ): Promise { return await axios.get(url, callParams.httpOptions).then( (res: Axios.AxiosResponse): T_Response => { if (DyNTS_global_settings.log_settings.api_events) { if (DyNTS_global_settings.log_settings.api_responseContents) { DyFM_Log.success(`"${callParams.name}" api call was successful`, res.data); } else { DyFM_Log.success(`"${callParams.name}" api call was successful`); } } if (callParams.getFullResponse) { return res as T_Response; } else { return res.data; } } ); } private static logEvent( url: string, inputParams: DyNTS_ApiCallInput_Params, callParams: DyNTS_ApiCall_Params ): void { if (DyNTS_global_settings.log_settings.api_requestContents) { DyFM_Log.log( `<--- outgoing API call: "${callParams?.name}" ` + `\n(${url})` + `\nbody:`, inputParams?.body ); } else { DyFM_Log.log(`<--- outgoing API call: "${callParams?.name}"`); } if (DyNTS_global_settings.log_settings.api_requestSettings) { DyFM_Log.log(`callParams:`, callParams); DyFM_Log.log('inputParams:', { pathParams: inputParams.pathParams ?? {}, body: inputParams.body ?? null, }); } } private static setupAxiosConfig( callParams: DyNTS_ApiCall_Params, axios: Axios.AxiosInstance ): void { // Ensure axios.defaults exists if (!axios.defaults) { DyFM_Log.error( `ERROR: axios.defaults is undefined in setupAxiosConfig for call (cant set headers and other options): "${callParams.name}"`, '\n stack:', callParams.stack ); return; } // Set headers if (callParams.httpOptions?.headers) { axios.defaults.headers.common ??= {}; for (const headerKey in callParams.httpOptions.headers) { if (!callParams.httpOptions.headers[headerKey]) { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'setupHeaders', new Error(`missing header: ${headerKey}`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-SH0`, }); } axios.defaults.headers.common[headerKey] = callParams.httpOptions.headers[headerKey]; } } // Set other Axios configuration options if (callParams.httpOptions?.httpsAgent) { axios.defaults.httpsAgent = callParams.httpOptions.httpsAgent; } if (callParams.httpOptions?.httpAgent) { axios.defaults.httpAgent = callParams.httpOptions.httpAgent; } if (callParams.httpOptions?.timeout !== undefined) { axios.defaults.timeout = callParams.httpOptions.timeout; } if (callParams.httpOptions?.maxRedirects !== undefined) { axios.defaults.maxRedirects = callParams.httpOptions.maxRedirects; } if (callParams.httpOptions?.withCredentials !== undefined) { axios.defaults.withCredentials = callParams.httpOptions.withCredentials; } if (callParams.httpOptions?.responseType !== undefined) { axios.defaults.responseType = callParams.httpOptions.responseType; } } private static setupPathParams( pathParams: { [param: string]: string; }, url: string, callParams: DyNTS_ApiCall_Params ): string { for (const pathParamKey in pathParams) { if (pathParams[pathParamKey]) { const paramType = typeof pathParams[pathParamKey]; switch (paramType) { case 'number': url = url.replace( `:${pathParamKey}`, pathParams[pathParamKey].toString() ); break; case 'string': url = url.replace(`:${pathParamKey}`, pathParams[pathParamKey]); break; default: /* DyFM_Log.error( `DYNAMO-NTS ERROR:`+ `\nwrong path param type: (${pathParamKey}): ${paramType} ` + `\n${pathParams[pathParamKey]}` + `\nMUST BE string or number` + `\n\n`, new Error() ); */ throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'setupPathParams', new Error( `wrong path param type: (${pathParamKey}): "${paramType}" (${callParams.name})` + `\n${pathParams[pathParamKey]}` ) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-SP1`, additionalContent: { pathParams: pathParams, url: url, }, }); } } else { throw new DyFM_Error({ ...this._getDefaultErrorSettings( 'setupPathParams', new Error(`missing pathParam: "${pathParamKey}" (${callParams.name})`) ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-SP0`, }); } } return url; } private static _getDefaultErrorSettings( fnName: string, error: DyFM_AnyError ): DyFM_Error_Settings { return { status: (error as DyFM_Error)?.___status ?? 500, message: (error as Error)?.message ?? (error as DyFM_Error)?._message ?? `${fnName} was UNSUCCESSFUL (NTS)`, addECToUserMsg: !(error as DyFM_Error)?.__userMessage, userMessage: (error as DyFM_Error)?.__userMessage ?? this.defaultErrorUserMsg, issuerService: `${this?.constructor?.name}-${this?.name}-DyNTS_ApiService`, error: error, }; } }