UNPKG

coddyger

Version:

Coddyger est une bibliothèque JavaScript/TypeScript qui fournit des fonctions communes et des plugins pour la gestion des données, la communication entre services, et des utilitaires avancés pour le développement d'applications.

367 lines (319 loc) 9.65 kB
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; import { LoggerService } from './logger.service'; interface AxiosServiceConfig extends AxiosRequestConfig { baseURL: string; timeout?: number; headers?: Record<string, string>; withCredentials?: boolean; retryConfig?: { maxRetries: number; delayMs: number; retryCondition?: (error: AxiosError) => boolean; }; cacheConfig?: { enabled: boolean; ttlMs?: number; }; } interface ApiError { message: string; code: string; status: number; errors?: any; timestamp?: string; path?: string; } interface ErrorResponse { error: ApiError; timestamp: string; path: string; } interface CacheItem<T> { data: T; timestamp: number; ttl: number; } export class AxiosService { private static instance: AxiosInstance; private static cache: Map<string, CacheItem<any>> = new Map(); private static retryConfig: Required<AxiosServiceConfig['retryConfig']> = { maxRetries: 3, delayMs: 1000, retryCondition: (error: AxiosError) => { // Retry on network errors and 5xx server errors return !error.response || (error.response.status >= 500 && error.response.status < 600); } }; static connect(config: AxiosServiceConfig): AxiosInstance { // Merge default retry config with provided config if (config.retryConfig) { this.retryConfig = { ...this.retryConfig, ...config.retryConfig }; } this.instance = axios.create({ baseURL: config.baseURL, timeout: config.timeout || 30000, headers: { 'Content-Type': 'application/json', ...config.headers, }, withCredentials: config.withCredentials || false, ...config, }); this.setupInterceptors(); return this.instance; } private static setupInterceptors(): void { // Request interceptor with logging this.instance.interceptors.request.use( (config) => { const requestId = Math.random().toString(36).substring(7); config.headers['X-Request-ID'] = requestId; LoggerService.debug('API Request', { requestId, method: config.method?.toUpperCase(), url: config.url, headers: this.sanitizeHeaders(config.headers), data: config.data }); return config; }, (error) => { LoggerService.error('API Request Error', { error }); return Promise.reject(error); } ); // Response interceptor with logging this.instance.interceptors.response.use( (response) => { const requestId = response.config.headers['X-Request-ID']; LoggerService.debug('API Response', { requestId, status: response.status, headers: this.sanitizeHeaders(response.headers), data: response.data }); return response; }, (error: AxiosError) => { const requestId = error.config?.headers['X-Request-ID']; const errorResponse = this.handleAxiosError(error); LoggerService.error('API Response Error', { requestId, error: errorResponse }); return Promise.reject(errorResponse); } ); } private static sanitizeHeaders(headers: any): any { const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key']; const sanitized = { ...headers }; for (const header of sensitiveHeaders) { if (sanitized[header]) { sanitized[header] = '[REDACTED]'; } } return sanitized; } private static handleAxiosError(error: AxiosError): ApiError { const defaultError: ApiError = { message: 'Une erreur inattendue s\'est produite', code: 'UNKNOWN_ERROR', status: 500, timestamp: new Date().toISOString() }; if (!error.response) { return { message: 'Erreur réseau - veuillez vérifier votre connexion', code: 'NETWORK_ERROR', status: 0, timestamp: new Date().toISOString() }; } const status = error.response.status; const data = error.response.data as ErrorResponse; const timestamp = new Date().toISOString(); const path = error.config?.url || ''; const errorMap: Record<number, ApiError> = { 400: { message: data?.error?.message || 'Requête invalide', code: 'BAD_REQUEST', status: 400, errors: data?.error?.errors }, 401: { message: 'Votre session a expiré. Veuillez vous reconnecter', code: 'UNAUTHORIZED', status: 401 }, 403: { message: 'Vous n\'avez pas les permissions nécessaires', code: 'FORBIDDEN', status: 403 }, 404: { message: 'La ressource demandée n\'a pas été trouvée', code: 'NOT_FOUND', status: 404 }, 422: { message: data?.error?.message || 'Erreur de validation', code: 'VALIDATION_ERROR', status: 422, errors: data?.error?.errors }, 429: { message: 'Trop de requêtes. Veuillez réessayer plus tard', code: 'RATE_LIMIT_EXCEEDED', status: 429 }, 500: { message: 'Erreur serveur interne. Veuillez réessayer plus tard', code: 'SERVER_ERROR', status: 500 }, 503: { message: 'Service temporairement indisponible', code: 'SERVICE_UNAVAILABLE', status: 503 } }; const errorResponse = errorMap[status] || defaultError; return { ...errorResponse, timestamp, path }; } private static getCacheKey(method: string, url: string, data?: any): string { return `${method}:${url}:${data ? JSON.stringify(data) : ''}`; } private static getFromCache<T>(cacheKey: string): T | null { const cached = this.cache.get(cacheKey); if (!cached) return null; const now = Date.now(); if (now - cached.timestamp > cached.ttl) { this.cache.delete(cacheKey); return null; } return cached.data; } private static setCache<T>(cacheKey: string, data: T, ttlMs: number): void { this.cache.set(cacheKey, { data, timestamp: Date.now(), ttl: ttlMs }); } static async withRetry<T>( requestFn: () => Promise<AxiosResponse<T>>, customConfig?: Partial<typeof this.retryConfig> ): Promise<AxiosResponse<T>> { const config = { ...this.retryConfig, ...customConfig }; let lastError: any; for (let attempt = 0; attempt < config.maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error; if (!config.retryCondition(error)) { break; } if (attempt < config.maxRetries - 1) { const delay = config.delayMs * Math.pow(2, attempt); LoggerService.warn(`Retry attempt ${attempt + 1}/${config.maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } // Utility methods with improved type safety and caching support static async get<T>( url: string, config?: AxiosRequestConfig & { retry?: boolean; cache?: boolean; cacheTTL?: number; } ): Promise<AxiosResponse<T>> { const cacheKey = this.getCacheKey('GET', url); if (config?.cache) { const cached = this.getFromCache<AxiosResponse<T>>(cacheKey); if (cached) return cached; } const request = () => this.instance.get<T>(url, config); const response = await (config?.retry ? this.withRetry(request) : request()); if (config?.cache) { this.setCache(cacheKey, response, config.cacheTTL || 5 * 60 * 1000); // Default 5 minutes } return response; } static async post<T>( url: string, data?: any, config?: AxiosRequestConfig & { retry?: boolean } ): Promise<AxiosResponse<T>> { const request = () => this.instance.post<T>(url, data, config); return config?.retry ? this.withRetry(request) : request(); } static async put<T>( url: string, data?: any, config?: AxiosRequestConfig & { retry?: boolean } ): Promise<AxiosResponse<T>> { const request = () => this.instance.put<T>(url, data, config); return config?.retry ? this.withRetry(request) : request(); } static async delete<T>( url: string, config?: AxiosRequestConfig & { retry?: boolean } ): Promise<AxiosResponse<T>> { const request = () => this.instance.delete<T>(url, config); return config?.retry ? this.withRetry(request) : request(); } static async patch<T>( url: string, data?: any, config?: AxiosRequestConfig & { retry?: boolean } ): Promise<AxiosResponse<T>> { const request = () => this.instance.patch<T>(url, data, config); return config?.retry ? this.withRetry(request) : request(); } // Enhanced header management static setHeader(key: string, value: string, type: 'common' | 'get' | 'post' | 'put' | 'patch' | 'delete' = 'common'): void { if (this.instance) { if (type === 'common') { this.instance.defaults.headers.common[key] = value; } else { this.instance.defaults.headers[type][key] = value; } } } static removeHeader(key: string, type: 'common' | 'get' | 'post' | 'put' | 'patch' | 'delete' = 'common'): void { if (this.instance) { if (type === 'common') { delete this.instance.defaults.headers.common[key]; } else { delete this.instance.defaults.headers[type][key]; } } } static getInstance(): AxiosInstance { if (!this.instance) { throw new Error('Instance Axios non initialisée. Appelez connect() d\'abord.'); } return this.instance; } // Utility method to clear cache static clearCache(): void { this.cache.clear(); } // Utility method to get cache size static getCacheSize(): number { return this.cache.size; } }