import axios from 'axios'; import type { AxiosInstance, RawAxiosRequestHeaders, AxiosRequestConfig, AxiosError } from 'axios'; import { DISCOVERY_ROUTE, DISCOVERY_ROUTE_ACCOUNTID, DISCOVERY_ROUTE_PROVIDERS_ACCOUNT_ID, TIMEOUT_SIGNAL_REASON, SdkStatus, } from './constants'; import { ApiTimeoutError } from './errors'; import { BaseConfig, BaseInterceptors, CalApiParams, MakeApiUrlData, ServiceDiscoveryResponse } from './types'; import { normalizeRoute, normalizeUrl, paramsSerializer, parseAxiosError, getServiceUrlWithRoute } from './utils'; export abstract class Base { protected token: string | (() => string); private readonly axios: AxiosInstance; private readonly discoveryUrl: string | null = null; private readonly serviceKey: string; private readonly requestAccountId: boolean = false; private readonly requestProvidersAccountId: boolean = false; protected readonly feature: string | null; private readonly _isCrossAccount: boolean = false; private status: SdkStatus = SdkStatus.NEW; private _currentAccountId: string | null = null; private _targetAccountId: string | null = null; private _providersAccountId: string | null = null; private _serviceUrl: string | null = null; protected constructor({ token, discoveryUrl, serviceKey, requestAccountId, requestProvidersAccountId, feature, accountId, serviceUrl, useDefaultSerializer, paramSerializer = paramsSerializer, httpAgents = {}, interceptors = {}, }: BaseConfig) { this.token = token; this.discoveryUrl = discoveryUrl || null; this.serviceKey = serviceKey; this.axios = axios.create({ ...(useDefaultSerializer ? {} : { paramsSerializer: { serialize: paramSerializer }, }), ...httpAgents, }); this._serviceUrl = serviceUrl || null; this.injectInterceptors(interceptors); if (accountId) { this._currentAccountId = accountId; this._targetAccountId = accountId; this._isCrossAccount = true; } else { this.requestAccountId = requestAccountId || false; } this.requestProvidersAccountId = requestProvidersAccountId || false; this.feature = feature || null; } /** * @deprecated used for migration stage only * @ignore */ public get currentAccountId(): string | null { return this._currentAccountId; } /** @ignore */ public get targetAccountId(): string | null { return this._targetAccountId; } /** * @deprecated used for migration stage only * @ignore */ public get providersAccountId(): string | null { return this._providersAccountId; } /** @ignore */ public get isCrossAccount(): boolean { return this._isCrossAccount; } /** @ignore */ async init(): Promise { try { if (this.serviceUrl) { this._serviceUrl = this.makeApiUrl({ url: normalizeUrl(this.serviceUrl) }); } else if (this.discoveryUrl) { const [ data, accountId, providersAccountId, ]: [ ServiceDiscoveryResponse, ...(string | null)[], ] = await Promise.all([ this.getServiceData(), this.requestAccountId ? this.getAccountId() : null, this.requestProvidersAccountId ? this.getProvidersAccId() : null, ]); if (this.requestAccountId) this._currentAccountId = accountId; if (this.requestProvidersAccountId) this._providersAccountId = providersAccountId; this._serviceUrl = this.makeApiUrl({ ...data, ... (accountId ? { accountId } : {}), }); } else { throw new Error(`Url for '${this.serviceKey}' or 'discoveryUrl' in missing`); } this.status = SdkStatus.SUCCESS; } catch (e) { this.status = SdkStatus.ERROR; throw new Error(`${this.getErrorSourcePrefix()} ${e instanceof Error ? e.message : 'Unknown initialization error'}`, { cause: e }); } } private injectInterceptors({ request, response }: BaseInterceptors) { if (request) { const requestInterceptors = Array.isArray(request) ? request : [request]; requestInterceptors.forEach((i) => this.axios.interceptors.request.use(i)); } if (response) { const responseInterceptors = Array.isArray(response) ? response : [response]; responseInterceptors.forEach((i) => this.axios.interceptors.response.use(i)); } } /** @ignore */ private async getServiceData(): Promise { const { data } = await this.axios({ url: `${normalizeUrl(this.discoveryUrl!)}/${this.normalizeRoute(DISCOVERY_ROUTE)}`, params: { serviceName: this.serviceKey, ...(this.feature ? { feature: this.feature } : {}), }, }); return data; } /** @ignore */ private async getAccountId(): Promise { const { data: { accountId } } = await this.axios({ url: `${normalizeUrl(this.discoveryUrl!)}/${this.normalizeRoute(DISCOVERY_ROUTE_ACCOUNTID)}`, headers: this.getHeaders(), }); return accountId; } /** @ignore */ private async getProvidersAccId(): Promise { const { data: { providersAccountId } } = await this.axios({ url: `${normalizeUrl(this.discoveryUrl!)}/${this.normalizeRoute(DISCOVERY_ROUTE_PROVIDERS_ACCOUNT_ID)}`, headers: this.getHeaders(), }); return providersAccountId; } /** @ignore */ public get serviceUrl(): string | null { return this._serviceUrl; } /** * * Override this method in case if api url has specific url arguments * @ignore */ makeApiUrl(data: MakeApiUrlData): string { return data.url; } /** * Method for parsing errors thrown from callApi request * * Override this method in case if you need specific error handling * @ignore */ parseError(e: AxiosError): Error { return parseAxiosError(e, this.constructor.name); } private getErrorSourcePrefix(): string { return `[${this.constructor.name}]`; } /** @ignore */ protected getHeaders(): RawAxiosRequestHeaders { return { 'Content-Type': 'application/json;charset=UTF-8', ... this.getToken() ? { Authorization: this.getToken() } : {}, }; } /** @ignore */ protected getToken(): string { if (typeof this.token === 'string') { return this.token; } if (typeof this.token === 'function') { return this.token(); } throw new Error('token is not defined'); } /** * Make API request * ```typescript * const result = await instance.callApi({ * route: '/route', * method: 'GET', * data: requestData, * params: queryParams, * timeout: 2500, * }); * ``` * @deprecated use callApiV2 * @ignore */ protected async callApi(params: CalApiParams): Promise { let timeoutTimer: NodeJS.Timeout | undefined; if (this.status === SdkStatus.NEW) { await this.init(); } try { let timeoutController: AbortController | undefined; let signal = params.signal; if (params.timeout && !params.signal) { timeoutController = new AbortController(); signal = timeoutController.signal; } const conf: AxiosRequestConfig = { url: this.getUrl(params), method: params.method || 'GET', data: params.data, params: params.params, headers: { ...this.getHeaders(), ...params.customHeaders, }, signal, withCredentials: params.withCredentials, timeout: params.timeout, }; if (params.timeout && timeoutController) { timeoutTimer = setTimeout( () => timeoutController?.abort(new ApiTimeoutError(TIMEOUT_SIGNAL_REASON)), params.timeout, ); } const { data } = await this.axios(conf); return data; } catch (e) { throw this.parseError(e as AxiosError); } finally { if (timeoutTimer) clearTimeout(timeoutTimer); } } /** * Make API request * ```typescript * const result = await instance.callApi({ * route: '/route', * method: 'GET', * data: requestData, * params: queryParams, * timeout: 2500, * }); * ``` * @ignore */ protected async callApiV2(params: CalApiParams): Promise { let timeoutTimer: NodeJS.Timeout | undefined; if (this.status === SdkStatus.NEW) { await this.init(); } try { let timeoutController: AbortController | undefined; let signal = params.signal; if (params.timeout && !params.signal) { timeoutController = new AbortController(); signal = timeoutController.signal; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const query: any = params.params || {}; if (this.isCrossAccount) { query.accountId = this.targetAccountId; } const axiosConfig: AxiosRequestConfig = { url: this.getUrl(params), method: params.method || 'GET', data: params.data, params: query, headers: { ...this.getHeaders(), ...params.customHeaders, }, signal, withCredentials: params.withCredentials, timeout: params.timeout, }; if (params.timeout && timeoutController) { timeoutTimer = setTimeout( () => timeoutController?.abort(new ApiTimeoutError(TIMEOUT_SIGNAL_REASON)), params.timeout, ); } const { data } = await this.axios(axiosConfig); return data; } catch (error) { throw this.parseError(error as AxiosError); } finally { if (timeoutTimer) clearTimeout(timeoutTimer); } } /** * Normalize route ro remove spaces from param * ```typescript * const result = await instance.normalizeRoute('/route'); * ``` * @ignore */ protected normalizeRoute(data: string): string { return normalizeRoute(data); } /** * Normalize URL for API request * @param {CalApiParams} params request params * @returns {string} normalized URL * @ignore */ protected getUrl(params: CalApiParams): string { if (params.url) return params.url; if (!this.serviceUrl) throw new Error('serviceUrl is not defined or not discovered'); return getServiceUrlWithRoute(this.serviceUrl, this.normalizeRoute(params.route)); } }