@blue-impact-engine/blue-impact-engine-client
Version:
Blue Impact Engine API Client
347 lines • 12.5 kB
JavaScript
/**
* Core HTTP client for Blue Impact Engine API
* Handles authentication, request/response processing, and error handling
*/
import axios from 'axios';
import { ApiErrorType, BlueImpactApiError, } from './types';
/**
* HTTP client for Blue Impact Engine API
* Provides a robust interface for making API calls with built-in error handling,
* retry logic, and response validation.
*/
export class HttpClient {
constructor(config) {
this.retryCount = 0;
this.config = {
timeout: 30000,
retries: 3,
retryDelay: 1000,
debug: false,
ssl: {
rejectUnauthorized: true, // Default to strict SSL validation
},
...config,
};
// Handle SSL configuration for Node.js environments
const axiosConfig = {
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...this.config.headers,
},
};
// Add SSL configuration for Node.js environments
if (this.config.ssl?.rejectUnauthorized === false) {
// Node.js environment - we can configure HTTPS agent
const https = require('https');
axiosConfig.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
this.client = axios.create(axiosConfig);
this.setupInterceptors();
}
/**
* Setup request and response interceptors
*/
setupInterceptors() {
// Request interceptor
this.client.interceptors.request.use((config) => {
if (this.config.apiKey) {
config.headers.Authorization = `Bearer ${this.config.apiKey}`;
}
if (this.config.debug) {
console.log(`[HTTP Client] ${config.method?.toUpperCase()} ${config.url}`, {
headers: config.headers,
data: config.data,
});
}
return config;
}, (error) => {
if (this.config.debug) {
console.error('[HTTP Client] Request error:', error);
}
return Promise.reject(error);
});
// Response interceptor
this.client.interceptors.response.use((response) => {
if (this.config.debug) {
console.log(`[HTTP Client] Response ${response.status}:`, response.data);
}
return response;
}, (error) => {
return this.handleResponseError(error);
});
}
/**
* Handle response errors with proper error mapping
*/
handleResponseError(error) {
let apiError;
if (error.response) {
// Server responded with error status
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 400:
apiError = new BlueImpactApiError(ApiErrorType.VALIDATION_ERROR, data?.message || 'Bad request', status, data?.details);
break;
case 401:
apiError = new BlueImpactApiError(ApiErrorType.AUTHENTICATION_ERROR, data?.message || 'Authentication required', status);
break;
case 403:
apiError = new BlueImpactApiError(ApiErrorType.AUTHORIZATION_ERROR, data?.message || 'Access denied', status);
break;
case 404:
apiError = new BlueImpactApiError(ApiErrorType.NOT_FOUND_ERROR, data?.message || 'Resource not found', status);
break;
case 409:
apiError = new BlueImpactApiError(ApiErrorType.CONFLICT_ERROR, data?.message || 'Resource conflict', status, data?.details);
break;
case 429:
apiError = new BlueImpactApiError(ApiErrorType.RATE_LIMIT_ERROR, data?.message || 'Rate limit exceeded', status);
break;
case 500:
case 502:
case 503:
case 504:
apiError = new BlueImpactApiError(ApiErrorType.SERVER_ERROR, data?.message || 'Server error', status);
break;
default:
apiError = new BlueImpactApiError(ApiErrorType.UNKNOWN_ERROR, data?.message || `HTTP ${status} error`, status);
}
}
else if (error.request) {
// Network error - check for SSL certificate issues
let errorMessage = 'Network error - no response received';
// Check if this is an SSL certificate error (common in development)
if (error.message && (error.message.includes('ERR_CERT_AUTHORITY_INVALID') ||
error.message.includes('certificate') ||
error.message.includes('SSL') ||
error.code === 'CERT_HAS_EXPIRED' ||
error.code === 'ERR_TLS_CERT_ALTNAME_INVALID')) {
errorMessage = 'SSL Certificate Error: The server\'s SSL certificate is not trusted. ' +
'This is common in development environments with self-signed certificates. ' +
'Please contact your administrator or try using HTTP instead of HTTPS for development.';
}
apiError = new BlueImpactApiError(ApiErrorType.NETWORK_ERROR, errorMessage, undefined, { originalError: error.message, isSSLError: error.message?.includes('CERT') });
}
else {
// Other error
apiError = new BlueImpactApiError(ApiErrorType.UNKNOWN_ERROR, error.message || 'Unknown error occurred');
}
if (this.config.debug) {
console.error('[HTTP Client] API Error:', apiError);
}
throw apiError;
}
appendQueryFilterParam(key, selectorParam, sp) {
if (!key || !selectorParam || !selectorParam.field || !selectorParam.operation || !selectorParam.value) {
return;
}
sp.append(`${key}[${selectorParam.field}][${selectorParam.operation}]`, selectorParam.value);
}
appendQueryParam(key, value, sp) {
if (!key || !value)
return;
// Expand arrays as multiple entries: ?tag=a&tag=b
if (Array.isArray(value)) {
for (const v of value)
this.appendQueryParam(key, v, sp);
return;
}
// Dates: ISO
if (value instanceof Date) {
sp.append(key, value.toISOString());
return;
}
// Objects: JSON (unless you need a custom format)
if (typeof value === "object") {
sp.append(key, JSON.stringify(value));
return;
}
// Primitives
sp.append(key, String(value));
}
/**
* Build query string from parameters
*/
buildQueryString(params) {
if (!params)
return '';
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (key === "where" || key === "select") {
this.appendQueryFilterParam(key, value, searchParams);
}
else {
this.appendQueryParam(key, value, searchParams);
}
});
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
/**
* Execute request with retry logic
*/
async executeRequest(config, options) {
const maxRetries = options?.retries ?? this.config.retries ?? 3;
const retryDelay = options?.retryDelay ?? this.config.retryDelay ?? 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await this.client.request(config);
return response.data;
}
catch (error) {
const isLastAttempt = attempt === maxRetries;
const shouldRetry = this.shouldRetry(error, attempt, maxRetries);
if (isLastAttempt || !shouldRetry) {
throw error;
}
if (this.config.debug) {
console.log(`[HTTP Client] Retry attempt ${attempt + 1}/${maxRetries}`);
}
await this.delay(retryDelay * Math.pow(2, attempt)); // Exponential backoff
}
}
throw new BlueImpactApiError(ApiErrorType.UNKNOWN_ERROR, 'Max retries exceeded');
}
/**
* Determine if a request should be retried
*/
shouldRetry(error, attempt, maxRetries) {
if (attempt >= maxRetries)
return false;
// Don't retry client errors (4xx) except for rate limiting
if (error instanceof BlueImpactApiError) {
return error.type === ApiErrorType.RATE_LIMIT_ERROR ||
error.type === ApiErrorType.SERVER_ERROR ||
error.type === ApiErrorType.NETWORK_ERROR;
}
// Retry network errors and server errors
if (error.code === 'ECONNABORTED' || error.code === 'ENOTFOUND') {
return true;
}
return false;
}
/**
* Delay execution for retry logic
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Make a GET request
*/
async get(endpoint, params, options) {
const url = `${endpoint}${this.buildQueryString(params)}`;
return this.executeRequest({
method: 'GET',
url,
headers: options?.headers,
timeout: options?.timeout,
}, options);
}
/**
* Make a POST request
*/
async post(endpoint, data, options) {
return this.executeRequest({
method: 'POST',
url: endpoint,
data,
headers: options?.headers,
timeout: options?.timeout,
}, options);
}
/**
* Make a PUT request
*/
async put(endpoint, data, options) {
return this.executeRequest({
method: 'PUT',
url: endpoint,
data,
headers: options?.headers,
timeout: options?.timeout,
}, options);
}
/**
* Make a PATCH request
*/
async patch(endpoint, data, options) {
return this.executeRequest({
method: 'PATCH',
url: endpoint,
data,
headers: options?.headers,
timeout: options?.timeout,
}, options);
}
/**
* Make a DELETE request
*/
async delete(endpoint, options) {
return this.executeRequest({
method: 'DELETE',
url: endpoint,
headers: options?.headers,
timeout: options?.timeout,
}, options);
}
/**
* Download a file
*/
async download(endpoint, params, options) {
const url = `${endpoint}${this.buildQueryString(params)}`;
const response = await this.client.request({
method: 'GET',
url,
responseType: 'blob',
headers: options?.headers,
timeout: options?.timeout,
});
return response.data;
}
/**
* Update client configuration
*/
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
if (newConfig.baseUrl) {
this.client.defaults.baseURL = newConfig.baseUrl;
}
if (newConfig.timeout) {
this.client.defaults.timeout = newConfig.timeout;
}
if (newConfig.headers) {
this.client.defaults.headers = {
...this.client.defaults.headers,
...newConfig.headers,
};
}
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Set API key for authentication
*/
setApiKey(apiKey) {
this.config.apiKey = apiKey;
this.client.defaults.headers.Authorization = `Bearer ${apiKey}`;
}
/**
* Clear API key
*/
clearApiKey() {
this.config.apiKey = undefined;
delete this.client.defaults.headers.Authorization;
}
}
//# sourceMappingURL=http-client.js.map