bam-ticketing-sdk
Version:
SDK for B.A.M Ticketing API
186 lines (167 loc) • 5.06 kB
text/typescript
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
)
}