UNPKG

@blue-impact-engine/blue-impact-engine-client

Version:
347 lines 12.5 kB
/** * 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