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
JavaScript
"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