UNPKG

bam-ticketing-sdk

Version:

SDK for B.A.M Ticketing API

186 lines (167 loc) 5.06 kB
import axiosRetry from 'axios-retry' import { AxiosCacheInstance, setupCache, CacheOptions, } from 'axios-cache-interceptor' import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' import { RetryOptions } from '../index' import { parseDates, stringifyDates, toCamelCase, toSnakeCase, } from './converter' import { ApiError } from './errors' // Constants export enum BaseUrl { Prod = 'https://api.bam.fan', Dev = 'https://develop.bam.fan', QA = 'https://qa.bam.fan', ThProd = 'https://api.tickethead.io', ThStaging = 'https://api.staging.tickethead.io', } export function createBaseUrl( baseUrl: string = BaseUrl.Prod, tenantName: string = undefined ) { const [protocol, base] = baseUrl.split('://') return `${protocol}://${tenantName?.concat('.') ?? ''}${base}` } /** * Sets the update request strategy for an axios instance */ export function setAxiosUpdateTokenFunction( instance: AxiosInstance, updateToken: () => Promise<string> ) { ;(instance as any).updateToken = updateToken } /** * Does nothing. Will cause 401 requests to fail as usual. */ async function nopUpdate(): Promise<string> { return '' } export interface IClientBuildOptions { baseUrl?: string tenantName?: string retryOptions?: RetryOptions cacheOptions?: CacheOptions camelCaseResponse?: boolean } /** * Builds an API client object. By default, creates a production client with cluster-wide access. * If `tenantName` is specified, the client includes the tenant name in the URL. * * @param baseUrl Base URL of the REST API (default: PROD_BASE_URL) * @param tenantName Tenant name (defaults to undefined) * @param retryOptions Configuration for the retry policy * @param cacheOptions Configuration for caching, by default no caching is done * @returns axios.AxiosInstance */ export function buildClient({ baseUrl = BaseUrl.Prod, tenantName, retryOptions = { retries: 3 }, cacheOptions, camelCaseResponse, }: IClientBuildOptions): AxiosInstance | AxiosCacheInstance { const axiosOptions: AxiosRequestConfig = { baseURL: createBaseUrl(baseUrl, tenantName), } let instance = axios.create(axiosOptions) if (cacheOptions) { instance = setupCache(instance, cacheOptions) } // Add a retry mechanism to all requests axiosRetry(instance, retryOptions) instance.defaults.headers.post['Content-Type'] = 'application/json' instance.defaults.headers.patch['Content-Type'] = 'application/json' instance.defaults.headers.put['Content-Type'] = 'application/json' // Sets the token update strategy setAxiosUpdateTokenFunction(instance, nopUpdate) // TODO: Add expiration check and update the token if needed instance.interceptors.request.use( function (config) { if ( !config.headers['Content-Type'] || config.headers['Content-Type'] === 'application/json' ) { config.data = stringifyDates(toSnakeCase(config.data)) } return config }, function (error) { Promise.reject(error) } ) instance.interceptors.response.use( function (response) { // If there is no need to convert case if ( !camelCaseResponse || ['stream', 'blob', 'arraybuffer'].includes(response.config.responseType) ) { return response } return { ...response, data: parseDates(toCamelCase(response.data)), } }, async function (error) { // Reject promise if usual error or it has been retried if (error?.response?.status !== 401 || error?.response?.config._retry) { // error.response.config._retry = false return Promise.reject(errorBuilder(error)) } // When response code is 401, try to refresh the token. try { // BEWARE of hack // Added updateToken function to axios instance because removing an interceptor is very hard // It enables changing of the token updating strategy on the fly const token = await (instance as any).updateToken() // Sets the retry flag error.response.config._retry = true error.response.config.headers['Authorization'] = 'Bearer ' + token return await instance(error.response.config) } catch (err) { return Promise.reject(errorBuilder(error)) } } ) return instance } /** * Builds a error instance based on the response HTTP status code * * @param error Error response from axios instance * @returns Error implementation */ function errorBuilder(error: any): ApiError { // The error is sometimes an error object and sometimes an axios error const message = error.response?.data?.message || error.message const errorCode = error.response?.data?.errorCode || error.response?.data?.error || error.error const status = error.response?.status || error.statusCode let url = error?.url if (error.config?.baseURL) { url = `${error.config?.baseURL}/${error.config?.url}` } const method = error.config?.method ?? error?.method const data = error.config?.data if (status) { return new ApiError(message, errorCode, status, url, method, data) } return new ApiError( 'Error in response from service', JSON.stringify(error), status, url, method, data ) }