UNPKG

guardz-axios

Version:

Type-safe HTTP client built on top of Axios with runtime validation using guardz. Part of the guardz ecosystem for comprehensive TypeScript type safety.

405 lines 15 kB
"use strict"; /** * Request Service - Core domain service for HTTP requests * Following Domain-Driven Design (DDD) principles */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RequestService = void 0; const types_1 = require("../domain/types"); const request_utils_1 = require("../utils/request-utils"); const validation_utils_1 = require("../utils/validation-utils"); const retry_utils_1 = require("../utils/retry-utils"); const error_utils_1 = require("../utils/error-utils"); /** * Request Service - Core domain service * Following Single Responsibility Principle and Dependency Injection */ class RequestService { constructor(config) { this.httpClient = config.httpClient; this.logger = config.logger || this.createDefaultLogger(); this.defaultTimeout = config.defaultTimeout || 5000; this.defaultRetryConfig = config.defaultRetryConfig || { attempts: 3, delay: 1000, backoff: "exponential", }; this.enableDebugLogging = config.enableDebugLogging || false; } /** * Executes a request with validation and retry logic * Main domain operation following DDD principles * * @param config - Complete request configuration * @returns Promise resolving to request result */ async executeRequest(config) { const startTime = Date.now(); try { this.logDebug("Starting request execution", { url: config.url, method: config.method, }); // Validate configuration const configError = this.validateConfiguration(config); if (configError) { this.logError("Configuration validation failed", { error: configError, config, }); return (0, request_utils_1.createErrorResult)(500, configError, "validation"); } // Execute with retry logic if configured if (config.retry) { this.logDebug("Executing request with retry logic", { retryConfig: config.retry, }); return this.executeWithRetry(config); } const result = await this.executeSingleRequest(config); const duration = Date.now() - startTime; this.logDebug("Request completed", { url: config.url, method: config.method, duration, status: result.status, }); return result; } catch (error) { const duration = Date.now() - startTime; this.logError("Unexpected error during request execution", { error, config, duration, }); return this.handleUnexpectedError(error, config); } } /** * Executes a single request without retry * Pure business logic following DDD principles * * @param config - Complete request configuration * @returns Promise resolving to request result */ async executeSingleRequest(config) { try { // Prepare HTTP request const httpConfig = this.prepareHttpConfig(config); this.logDebug("Executing HTTP request", { url: httpConfig.url, method: httpConfig.method, hasData: !!httpConfig.data, hasHeaders: !!httpConfig.headers, }); // Execute HTTP request const response = await this.httpClient.request(httpConfig); this.logDebug("HTTP request completed", { url: config.url, method: config.method, hasResponseData: !!response.data, }); // Validate response data return this.validateResponse(response, config); } catch (error) { this.logError("HTTP request failed", { error, url: config.url, method: config.method, }); return this.handleRequestError(error, config); } } /** * Executes request with retry logic * Business logic for retry behavior * * @param config - Complete request configuration * @returns Promise resolving to request result */ async executeWithRetry(config) { const retryStrategy = (0, retry_utils_1.createRetryStrategy)(config.retry); let lastError; for (let attempt = 1; attempt <= config.retry.attempts; attempt++) { try { this.logDebug(`Retry attempt ${attempt}/${config.retry.attempts}`, { url: config.url, method: config.method, }); const result = await this.executeSingleRequest(config); // If successful, return immediately if (result.status === types_1.RequestStatus.SUCCESS) { this.logDebug("Request succeeded on retry", { attempt, url: config.url, method: config.method, }); return result; } // If error result, check if we should retry if (result.status === types_1.RequestStatus.ERROR) { // Create error object with proper type information const errorObj = new Error(result.message); errorObj.type = result.type; errorObj.code = result.code; lastError = errorObj; if (!retryStrategy.shouldRetry(attempt, lastError)) { this.logDebug("Retry condition not met", { attempt, errorType: result.type, url: config.url, }); return result; } // Wait before retry if (attempt < config.retry.attempts) { const delay = retryStrategy.getDelay(attempt); this.logDebug(`Waiting ${delay}ms before retry`, { attempt, delay, }); await this.sleep(delay); } } } catch (error) { lastError = error; if (!retryStrategy.shouldRetry(attempt, error)) { this.logDebug("Retry condition not met for thrown error", { attempt, error: error instanceof Error ? error.message : String(error), }); return this.handleRequestError(error, config); } // Wait before retry if (attempt < config.retry.attempts) { const delay = retryStrategy.getDelay(attempt); this.logDebug(`Waiting ${delay}ms before retry after error`, { attempt, delay, }); await this.sleep(delay); } } } // All retries exhausted this.logError("All retry attempts exhausted", { attempts: config.retry.attempts, url: config.url, method: config.method, lastError, }); return this.handleRequestError(lastError, config); } /** * Validates response data using type guard * Pure business logic for validation * * @param response - HTTP response * @param config - Request configuration * @returns Request result with validated data or error */ validateResponse(response, config) { const responseData = response.data; this.logDebug("Validating response data", { url: config.url, method: config.method, tolerance: config.tolerance, }); if (config.tolerance) { // Tolerance mode - return data even if validation fails const { data, errors } = (0, validation_utils_1.validateDataWithTolerance)(responseData, config.guard, config.identifier); if (errors.length > 0) { this.logWarn("Validation warnings in tolerance mode", { errors, url: config.url, method: config.method, }); if (config.onError) { const context = (0, validation_utils_1.createValidationContext)("validation", config.url, config.method, undefined, { errors, data: responseData }); config.onError(`Validation warnings: ${errors.join(", ")}`, context); } } return (0, request_utils_1.createSuccessResult)(data); } else { // Strict validation const { isValid, validatedData, errors } = (0, validation_utils_1.validateData)(responseData, config.guard, config.identifier); if (isValid && validatedData) { return (0, request_utils_1.createSuccessResult)(validatedData); } else { const errorMessage = errors.length > 0 ? `Response data validation failed: ${errors.join(", ")}` : "Response data validation failed"; this.logError("Validation failed", { errors, url: config.url, method: config.method, }); return (0, request_utils_1.createErrorResult)(500, errorMessage, "validation"); } } } /** * Handles request errors with comprehensive error categorization * * @param error - Error that occurred * @param config - Request configuration * @returns Error result */ handleRequestError(error, config) { const errorType = (0, error_utils_1.categorizeError)(error); const message = (0, error_utils_1.extractErrorMessage)(error); const statusCode = (0, error_utils_1.extractHttpStatusCode)(error) || 500; const context = (0, error_utils_1.createErrorContext)(error, config.url, config.method); if (this.shouldLogError(errorType, statusCode)) { this.logger.error(`Request failed: ${message}`, context); } return (0, request_utils_1.createErrorResult)(statusCode, message, errorType); } /** * Handles unexpected errors * Business logic for unexpected error handling * * @param error - Unexpected error * @param config - Request configuration * @returns Error result */ handleUnexpectedError(error, config) { const message = (0, error_utils_1.extractErrorMessage)(error); this.logger.error(`Unexpected error: ${message}`, { config, error }); return (0, request_utils_1.createErrorResult)(500, message, "unknown"); } /** * Validates request configuration * * @param config - Configuration to validate * @returns Error message if invalid, null if valid */ validateConfiguration(config) { // Validate basic request config const requestError = (0, request_utils_1.validateRequestConfig)(config); if (requestError) { return requestError; } // Validate retry config if present if (config.retry) { const retryError = (0, retry_utils_1.validateRetryConfig)(config.retry); if (retryError) { return retryError; } } return null; } /** * Prepares HTTP configuration for axios * * @param config - Request configuration * @returns HTTP configuration object */ prepareHttpConfig(config) { const httpConfig = { url: config.url, method: config.method, timeout: config.timeout || this.defaultTimeout, }; if (config.data !== undefined) { httpConfig.data = config.data; } if (config.headers) { httpConfig.headers = { ...config.headers }; } if (config.baseURL) { httpConfig.baseURL = config.baseURL; } return httpConfig; } /** * Determines if error should be logged based on type and status * * @param errorType - Type of error * @param statusCode - HTTP status code * @returns Whether error should be logged */ shouldLogError(errorType, statusCode) { // Always log validation and unknown errors if (errorType === "validation" || errorType === "unknown") { return true; } // Log network and timeout errors if (errorType === "network" || errorType === "timeout") { return true; } // Log HTTP 5xx errors if (statusCode >= 500) { return true; } return false; } /** * Sleep utility for retry delays * * @param ms - Milliseconds to sleep * @returns Promise that resolves after delay */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Creates default logger implementation * * @returns Default logger instance */ createDefaultLogger() { return { error: (message, context) => { console.error(`[ERROR] ${message}`, context); }, warn: (message, context) => { console.warn(`[WARN] ${message}`, context); }, info: (message, context) => { console.info(`[INFO] ${message}`, context); }, debug: (message, context) => { if (this.enableDebugLogging) { console.debug(`[DEBUG] ${message}`, context); } }, }; } /** * Logs debug messages if debug logging is enabled * * @param message - Debug message * @param context - Debug context */ logDebug(message, context) { if (this.enableDebugLogging) { this.logger.debug(message, context); } } /** * Logs error messages * * @param message - Error message * @param context - Error context */ logError(message, context) { this.logger.error(message, context); } /** * Logs warning messages * * @param message - Warning message * @param context - Warning context */ logWarn(message, context) { this.logger.warn(message, context); } } exports.RequestService = RequestService; //# sourceMappingURL=request-service.js.map