@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
JavaScript
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;
}
}