autotrader-connect-api
Version:
Production-ready TypeScript wrapper for Auto Trader UK Connect APIs
384 lines • 13.8 kB
JavaScript
;
/**
* HTTP client for AutoTrader API
* Configures Axios with authentication, rate limiting, and error handling
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getClient = exports.ApiClient = void 0;
const axios_1 = __importDefault(require("axios"));
const bottleneck_1 = __importDefault(require("bottleneck"));
const auth_1 = require("./auth");
/**
* Default configuration values
*/
/**
* Determine if we should use sandbox based on environment
*/
function shouldUseSandbox() {
// Explicit sandbox flag takes precedence
if (process.env['AT_USE_SANDBOX'] !== undefined) {
return process.env['AT_USE_SANDBOX'] === 'true';
}
// Default to sandbox in development
return process.env['NODE_ENV'] === 'development' || process.env['NODE_ENV'] === 'test';
}
/**
* Get the appropriate base URL for the current environment
*/
function getBaseURL(useSandbox) {
const shouldSandbox = useSandbox !== undefined ? useSandbox : shouldUseSandbox();
if (shouldSandbox) {
return process.env['AT_SANDBOX_BASE_URL'] || 'https://sandbox-api.autotrader.co.uk';
}
return process.env['AT_BASE_URL'] || 'https://api.autotrader.co.uk';
}
/**
* Get the appropriate API credentials for the current environment
*/
function getApiCredentials(useSandbox) {
const shouldSandbox = useSandbox !== undefined ? useSandbox : shouldUseSandbox();
const result = {};
if (shouldSandbox) {
const apiKey = process.env['AT_SANDBOX_API_KEY'];
const apiSecret = process.env['AT_SANDBOX_API_SECRET'];
if (apiKey)
result.apiKey = apiKey;
if (apiSecret)
result.apiSecret = apiSecret;
}
else {
const apiKey = process.env['AT_API_KEY'];
const apiSecret = process.env['AT_API_SECRET'];
if (apiKey)
result.apiKey = apiKey;
if (apiSecret)
result.apiSecret = apiSecret;
}
return result;
}
const DEFAULT_CONFIG = {
baseURL: getBaseURL(),
timeout: parseInt(process.env['AT_TIMEOUT'] || '30000', 10),
maxRetries: 3,
rateLimitRequests: parseInt(process.env['AT_RATE_LIMIT_REQUESTS'] || '100', 10),
rateLimitWindow: parseInt(process.env['AT_RATE_LIMIT_WINDOW'] || '60000', 10),
debug: process.env['AT_DEBUG'] === 'true',
useSandbox: shouldUseSandbox(),
};
/**
* HTTP client class with rate limiting and authentication
*/
class ApiClient {
constructor(config) {
this.config = { ...DEFAULT_CONFIG, ...config };
// Initialize rate limiter
this.rateLimiter = new bottleneck_1.default({
reservoir: this.config.rateLimitRequests,
reservoirRefreshAmount: this.config.rateLimitRequests,
reservoirRefreshInterval: this.config.rateLimitWindow,
maxConcurrent: 10,
minTime: Math.floor(this.config.rateLimitWindow / this.config.rateLimitRequests),
retryCount: this.config.maxRetries,
retry: this.shouldRetryRequest.bind(this),
});
// Initialize Axios instance
this.axiosInstance = axios_1.default.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'AutoTrader-Connect-API/1.0.0',
},
});
// Setup interceptors
this.setupRequestInterceptor();
this.setupResponseInterceptor();
}
/**
* Make a GET request
*/
async get(url, options) {
return this.request('GET', url, undefined, options);
}
/**
* Make a POST request
*/
async post(url, data, options) {
return this.request('POST', url, data, options);
}
/**
* Make a PUT request
*/
async put(url, data, options) {
return this.request('PUT', url, data, options);
}
/**
* Make a PATCH request
*/
async patch(url, data, options) {
return this.request('PATCH', url, data, options);
}
/**
* Make a DELETE request
*/
async delete(url, options) {
return this.request('DELETE', url, undefined, options);
}
/**
* Make a generic HTTP request with rate limiting
*/
async request(method, url, data, options) {
return this.rateLimiter.schedule(async () => {
const config = {
method,
url,
data,
timeout: options?.timeout || this.config.timeout,
};
if (options?.params) {
config.params = options.params;
}
if (options?.headers) {
config.headers = options.headers;
}
try {
const response = await this.axiosInstance(config);
if (this.config.debug) {
console.log(`[AutoTrader API] ${method} ${url}:`, {
status: response.status,
data: response.data,
});
}
return response.data;
}
catch (error) {
if (this.config.debug) {
console.error(`[AutoTrader API] ${method} ${url} Error:`, error);
}
throw this.handleApiError(error);
}
});
}
/**
* Setup request interceptor for authentication
*/
setupRequestInterceptor() {
this.axiosInstance.interceptors.request.use(async (config) => {
try {
// Get and inject authentication token
const token = await (0, auth_1.getToken)();
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
if (this.config.debug) {
console.log(`[AutoTrader API] Request: ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
}
catch (error) {
console.error('Failed to get authentication token:', error);
return Promise.reject(error);
}
}, (error) => {
console.error('Request interceptor error:', error);
return Promise.reject(error);
});
}
/**
* Setup response interceptor for error handling and token refresh
*/
setupResponseInterceptor() {
this.axiosInstance.interceptors.response.use((response) => {
// Update rate limit info if available
this.updateRateLimitInfo(response);
return response;
}, async (error) => {
const originalRequest = error.config;
// Handle 401 Unauthorized - attempt token refresh and retry
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
originalRequest._retry = true;
try {
if (this.config.debug) {
console.log('[AutoTrader API] Token expired, attempting refresh...');
}
// Force token refresh
await (0, auth_1.getToken)();
// Retry the original request
const token = await (0, auth_1.getToken)();
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${token}`;
return this.axiosInstance(originalRequest);
}
catch (refreshError) {
console.error('Token refresh failed:', refreshError);
return Promise.reject(error);
}
}
return Promise.reject(error);
});
}
/**
* Update rate limit information from response headers
*/
updateRateLimitInfo(response) {
const rateLimitRemaining = response.headers['x-ratelimit-remaining'];
const rateLimitReset = response.headers['x-ratelimit-reset'];
if (rateLimitRemaining !== undefined) {
const remaining = parseInt(rateLimitRemaining, 10);
if (!isNaN(remaining) && remaining < 10) {
console.warn(`[AutoTrader API] Rate limit warning: ${remaining} requests remaining`);
}
}
if (rateLimitReset !== undefined) {
const resetTime = parseInt(rateLimitReset, 10);
if (!isNaN(resetTime)) {
const resetDate = new Date(resetTime * 1000);
if (this.config.debug) {
console.log(`[AutoTrader API] Rate limit resets at: ${resetDate.toISOString()}`);
}
}
}
}
/**
* Handle API errors and convert to standardized format
*/
handleApiError(error) {
if (axios_1.default.isAxiosError(error)) {
const axiosError = error;
// Extract error information
const status = axiosError.response?.status;
const statusText = axiosError.response?.statusText;
const data = axiosError.response?.data;
// Create detailed error message
let message = `API request failed: ${axiosError.message}`;
if (status) {
message += ` (${status}`;
if (statusText) {
message += ` ${statusText}`;
}
message += ')';
}
// Extract API error details if available
if (data && typeof data === 'object') {
const apiData = data;
if (apiData.message) {
message += ` - ${apiData.message}`;
}
if (apiData.error_description) {
message += ` - ${apiData.error_description}`;
}
}
// Create enhanced error object
const enhancedError = new Error(message);
enhancedError.status = status;
enhancedError.statusText = statusText;
enhancedError.response = axiosError.response;
enhancedError.request = axiosError.request;
enhancedError.isAxiosError = true;
return enhancedError;
}
// Return original error if not an Axios error
return error instanceof Error ? error : new Error(String(error));
}
/**
* Determine if a request should be retried
*/
shouldRetryRequest(error, retryCount) {
// Don't retry more than the configured maximum
if (retryCount >= this.config.maxRetries) {
return false;
}
// Check if it's an axios error
if (!error.isAxiosError) {
return false;
}
const status = error.status;
// Retry on 5xx server errors and 429 rate limit
if (status >= 500 || status === 429) {
// For rate limiting, use the retry-after header if available
if (status === 429) {
const retryAfter = error.response?.headers['retry-after'];
if (retryAfter) {
const delay = parseInt(retryAfter, 10);
return isNaN(delay) ? 1000 : delay * 1000; // Convert to milliseconds
}
}
// Exponential backoff for other retryable errors
return Math.min(1000 * Math.pow(2, retryCount), 10000);
}
// Don't retry 4xx client errors (except 429)
return false;
}
/**
* Get current rate limiter status
*/
getRateLimitStatus() {
return {
reservoir: this.rateLimiter.reservoir?.() || 0,
running: this.rateLimiter.running?.() || 0,
queued: this.rateLimiter.queued?.() || 0,
};
}
/**
* Get client configuration
*/
getConfig() {
return { ...this.config };
}
}
exports.ApiClient = ApiClient;
/**
* Default client instance
*/
let defaultClient = null;
/**
* Get or create the default API client
*/
function getClient(config) {
if (!defaultClient) {
if (!config) {
const useSandbox = shouldUseSandbox();
const credentials = getApiCredentials(useSandbox);
if (!credentials.apiKey || !credentials.apiSecret) {
const envPrefix = useSandbox ? 'AT_SANDBOX_' : 'AT_';
throw new Error(`API configuration required. Provide config or set ${envPrefix}API_KEY and ${envPrefix}API_SECRET environment variables. ` +
`Current environment: ${useSandbox ? 'sandbox' : 'production'}`);
}
config = {
...DEFAULT_CONFIG,
apiKey: credentials.apiKey,
apiSecret: credentials.apiSecret,
baseURL: getBaseURL(useSandbox),
useSandbox,
};
}
defaultClient = new ApiClient(config);
}
return defaultClient;
}
exports.getClient = getClient;
/**
* Default client instance export
*/
const client = {
get: (url, options) => {
return getClient().get(url, options);
},
post: (url, data, options) => {
return getClient().post(url, data, options);
},
put: (url, data, options) => {
return getClient().put(url, data, options);
},
patch: (url, data, options) => {
return getClient().patch(url, data, options);
},
delete: (url, options) => {
return getClient().delete(url, options);
},
};
exports.default = client;
//# sourceMappingURL=client.js.map