UNPKG

fetch-api-client

Version:

A TypeScript API client using fetch with axios-like interface

312 lines (311 loc) 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApiClient = void 0; exports.createClient = createClient; const interceptors_1 = require("./interceptors"); /** * Type guard functions remain the same... */ function isFetchTypeError(error) { return error instanceof TypeError && typeof error.message === 'string'; } function isAbortError(error) { return error instanceof Error && error.name === 'AbortError'; } function isApiError(error) { return typeof error === 'object' && error !== null && 'status' in error && 'message' in error; } /** * Main API Client class that provides axios-like interface using fetch */ class ApiClient { constructor(config = {}) { // Set default configuration with proper credentials this.config = { baseURL: '', timeout: 30000, headers: { 'Content-Type': 'application/json', }, getToken: () => null, credentials: 'include', // ✅ Always include cookies by default validateStatus: (status) => status >= 200 && status < 300, ...config, }; // Initialize interceptor managers this.interceptors = { request: new interceptors_1.RequestInterceptorManager(), response: new interceptors_1.ResponseInterceptorManager(), }; } /** * Build complete URL from base URL and endpoint */ buildUrl(url, params) { const fullUrl = url.startsWith('http') ? url : `${this.config.baseURL}${url}`; if (!params) return fullUrl; const urlObj = new URL(fullUrl); Object.entries(params).forEach(([key, value]) => { if (value !== null && value !== undefined) { urlObj.searchParams.append(key, String(value)); } }); return urlObj.toString(); } /** * Prepare headers for the request */ async prepareHeaders(config) { const headers = new Headers(); // Add default headers Object.entries(this.config.headers).forEach(([key, value]) => { headers.set(key, value); }); // Add request-specific headers if (config.headers) { Object.entries(config.headers).forEach(([key, value]) => { headers.set(key, value); }); } // Add authorization token if available try { const token = await this.config.getToken(); if (token) { headers.set('Authorization', `Bearer ${token}`); } } catch (error) { console.warn('Failed to get token:', error); } return headers; } /** * Prepare request body */ prepareBody(data, headers) { if (!data) return null; const contentType = headers.get('Content-Type'); if (data instanceof FormData) { // Remove Content-Type header for FormData to let browser set it headers.delete('Content-Type'); return data; } if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/json')) { return JSON.stringify(data); } if (contentType === null || contentType === void 0 ? void 0 : contentType.includes('application/x-www-form-urlencoded')) { const params = new URLSearchParams(); Object.entries(data).forEach(([key, value]) => { params.append(key, String(value)); }); return params.toString(); } return String(data); } /** * Create AbortController with timeout */ createAbortController(timeout) { const controller = new AbortController(); const timeoutMs = timeout !== null && timeout !== void 0 ? timeout : this.config.timeout; if (timeoutMs > 0) { setTimeout(() => { controller.abort(); }, timeoutMs); } return controller; } /** * Parse response based on content type */ async parseResponse(response) { const contentType = response.headers.get('Content-Type') || ''; if (contentType.includes('application/json')) { return response.json(); } if (contentType.includes('text/')) { return response.text(); } if (contentType.includes('application/octet-stream') || contentType.includes('image/')) { return response.blob(); } return response.text(); } /** * Create standardized error object */ createError(message, status, statusText, data, config) { return { message, status, statusText, data, config, code: status ? `HTTP_${status}` : 'NETWORK_ERROR', }; } /** * Core request method - Enhanced with CORS-friendly credentials handling */ async request(config) { var _a; try { // Process request through interceptors const processedConfig = await this.interceptors.request.execute(config); // Prepare request components const url = this.buildUrl(processedConfig.url || '', processedConfig.params); const headers = await this.prepareHeaders(processedConfig); const body = this.prepareBody(processedConfig.data, headers); const controller = processedConfig.signal ? { signal: processedConfig.signal } : this.createAbortController(processedConfig.timeout); // ✅ Use proper credentials configuration - avoid 'include' for CORS-sensitive requests const credentials = (_a = processedConfig.credentials) !== null && _a !== void 0 ? _a : this.config.credentials; // ✅ Clean headers to avoid CORS preflight triggers const cleanHeaders = new Headers(); headers.forEach((value, key) => { // Only include CORS-safe headers const corsUnsafeHeaders = [ 'x-ios-device', 'x-ios-version', 'x-platform', 'x-device-type', 'pragma', 'cache-control' ]; if (!corsUnsafeHeaders.includes(key.toLowerCase())) { cleanHeaders.set(key, value); } }); // Execute fetch request const response = await fetch(url, { method: processedConfig.method || 'GET', headers: cleanHeaders, // ✅ Use cleaned headers body, credentials, // ✅ Use proper fetch credentials API ...controller, }); // Parse response data const data = await this.parseResponse(response); // Check if response status is valid if (!this.config.validateStatus(response.status)) { const error = this.createError(`Request failed with status ${response.status}`, response.status, response.statusText, data, processedConfig); const processedError = await this.interceptors.response.executeRejected(error); throw processedError; } // Create successful response const apiResponse = { data, status: response.status, statusText: response.statusText, headers: response.headers, config: processedConfig, }; // Process response through interceptors return await this.interceptors.response.executeFulfilled(apiResponse); } catch (error) { // Handle different types of errors with proper type checking if (isFetchTypeError(error) && error.message.includes('fetch')) { const networkError = this.createError('Network Error', undefined, undefined, undefined, config); const processedError = await this.interceptors.response.executeRejected(networkError); throw processedError; } if (isAbortError(error)) { const timeoutError = this.createError('Request Timeout', undefined, undefined, undefined, config); const processedError = await this.interceptors.response.executeRejected(timeoutError); throw processedError; } // Re-throw if it's already our custom error if (isApiError(error)) { throw error; } // Create generic error for unknown error types const errorMessage = error instanceof Error ? error.message : 'Unknown Error'; const genericError = this.createError(errorMessage, undefined, undefined, undefined, config); const processedError = await this.interceptors.response.executeRejected(genericError); throw processedError; } } /** * HTTP methods remain the same but ensure credentials are properly passed */ async get(url, config) { var _a; return this.request({ ...config, method: 'GET', url, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async post(url, data, config) { var _a; return this.request({ ...config, method: 'POST', url, data, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async put(url, data, config) { var _a; return this.request({ ...config, method: 'PUT', url, data, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async delete(url, config) { var _a; return this.request({ ...config, method: 'DELETE', url, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async patch(url, data, config) { var _a; return this.request({ ...config, method: 'PATCH', url, data, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async head(url, config) { var _a; return this.request({ ...config, method: 'HEAD', url, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } async options(url, config) { var _a; return this.request({ ...config, method: 'OPTIONS', url, credentials: (_a = config === null || config === void 0 ? void 0 : config.credentials) !== null && _a !== void 0 ? _a : 'include' // Ensure credentials are included }); } } exports.ApiClient = ApiClient; /** * Create a new API client instance */ function createClient(config) { return new ApiClient(config); }