UNPKG

@ritas-inc/sapb1commandapi-client

Version:

A stateless TypeScript client for SAP B1 Service Layer Command API with comprehensive error handling, type safety, and batch operations

129 lines (128 loc) 5.06 kB
import axios from 'axios'; import axiosRetry from 'axios-retry'; import { z } from 'zod'; import { SAPB1APIError, AuthError, NetworkError, ValidationError, isErrorResponse } from './errors.js'; import { ErrorResponseSchema } from '../schemas/common.schema.js'; export class HTTPClient { client; constructor(config) { this.client = axios.create({ baseURL: config.baseUrl, timeout: config.timeout ?? 30000, headers: { ...config.headers } }); const retryConfig = config.retryConfig || {}; axiosRetry(this.client, { retries: retryConfig.retries ?? 3, retryDelay: retryConfig.retryDelay ?? axiosRetry.exponentialDelay, retryCondition: retryConfig.retryCondition ?? ((error) => { return axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response?.status ? error.response.status >= 500 : false); }) }); this.client.interceptors.response.use((response) => response, (error) => { if (error.response?.data) { try { const errorData = ErrorResponseSchema.parse(error.response.data); if (error.response.status === 401) { throw new AuthError(errorData.problem); } throw new SAPB1APIError(errorData.problem); } catch (e) { if (e instanceof SAPB1APIError || e instanceof AuthError) { throw e; } } } if (error.code === 'ECONNABORTED') { throw new NetworkError('Request timeout', error.code, error.config, error.response); } if (!error.response) { throw new NetworkError(error.message || 'Network error occurred', error.code, error.config, error.response); } throw error; }); } async request(config) { const headers = {}; if (config.headers) { Object.entries(config.headers).forEach(([key, value]) => { if (typeof value === 'string') { headers[key] = value; } else if (value != null) { headers[key] = String(value); } }); } if (config.userId) { headers['X-User-Id'] = config.userId; } if (config.data && (config.method === 'POST' || config.method === 'PATCH' || config.method === 'PUT')) { headers['Content-Type'] = 'application/json'; } if (process.env.INTEGRATION_TEST === 'true') { console.log(`[HTTP] [REQUEST] ${config.method?.toUpperCase()} ${config.url}`); if (config.data) { console.log(' Request Body:', JSON.stringify(config.data, null, 2)); } console.log(' Headers:', JSON.stringify(headers, null, 2)); } const response = await this.client.request({ ...config, headers }); if (process.env.INTEGRATION_TEST === 'true') { console.log(`[HTTP] [RESPONSE] ${response.status} ${response.statusText}`); console.log(' Response Body:', JSON.stringify(response.data, null, 2)); } return response.data; } async get(url, userId, config) { return this.request({ ...config, method: 'GET', url, userId }); } async post(url, data, userId, config) { return this.request({ ...config, method: 'POST', url, data, userId }); } async patch(url, data, userId, config) { return this.request({ ...config, method: 'PATCH', url, data, userId }); } async delete(url, userId, config) { return this.request({ ...config, method: 'DELETE', url, userId }); } async validateAndRequest(config, inputSchema, outputSchema) { if (inputSchema && config.data) { try { config.data = inputSchema.parse(config.data); } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError('Request validation failed', error.errors); } throw error; } } const response = await this.request(config); if (isErrorResponse(response)) { if (response.problem.status === 401) { throw new AuthError(response.problem); } throw new SAPB1APIError(response.problem); } if (outputSchema) { try { return outputSchema.parse(response.data); } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError('Response validation failed', error.errors); } throw error; } } return response.data; } }