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
text/typescript
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;
}
}