@ritas-inc/hanaqueryapi-client
Version:
TypeScript client for HANA Query API with full type safety and error handling
313 lines (276 loc) • 7.68 kB
text/typescript
/**
* HANA Query API Client - Configuration
*
* Default configuration and environment-specific settings for the API client.
*/
import type { ClientConfig } from './types.ts';
/**
* Default client configuration (excluding baseUrl which is required)
*/
export const DEFAULT_CONFIG = {
timeout: 30000, // 30 seconds
retries: 3,
retryDelay: 1000, // 1 second base delay
enableLogging: false,
logLevel: 'info' as const,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
/**
* Environment-specific configurations
*/
export const ENVIRONMENT_CONFIGS: Record<string, ClientConfig> = {
development: {
baseUrl: 'http://localhost:3001',
enableLogging: true,
logLevel: 'debug',
timeout: 10000
},
testing: {
baseUrl: 'http://localhost:3001',
enableLogging: true,
logLevel: 'warn',
timeout: 5000,
retries: 1
},
staging: {
baseUrl: 'https://api-staging.example.com',
enableLogging: true,
logLevel: 'info',
timeout: 20000
},
production: {
baseUrl: 'https://api.example.com',
enableLogging: false,
logLevel: 'error',
timeout: 30000
}
};
/**
* API version and paths
*/
export const API_VERSION = 'v1';
export const API_BASE_PATH = `/api/${API_VERSION}`;
/**
* Endpoint paths
*/
export const ENDPOINTS = {
HEALTH: '/health',
DOCS: '/docs',
ITEMS: '/items',
ITEMS_GROUPS: '/items/groups',
ITEMS_HIERARCHIES: '/items/hierarchies',
ITEMS_TREES: '/items/trees',
SALES: '/sales',
PRODUCTION_SECTORS: '/production-sectors',
PLANS: '/plans',
ALL_PLANS_SECTORS: '/plans/sectors',
PLAN_DETAIL: '/plans/{planId}',
PLAN_PRODUCTS: '/plans/{planId}/products',
PLAN_WORK_ORDERS: '/plans/{planId}/work-orders',
PLAN_SECTORS: '/plans/{planId}/sectors',
USER: '/users/{username}'
} as const;
/**
* HTTP status codes
*/
export const HTTP_STATUS = {
OK: 200,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504
} as const;
/**
* Request timeout configuration for different endpoint types
*/
export const ENDPOINT_TIMEOUTS = {
// Fast endpoints
[ENDPOINTS.HEALTH]: 5000,
[ENDPOINTS.DOCS]: 5000,
[ENDPOINTS.PLANS]: 15000,
[ENDPOINTS.ALL_PLANS_SECTORS]: 30000,
[ENDPOINTS.PLAN_DETAIL]: 10000,
[ENDPOINTS.PLAN_PRODUCTS]: 10000,
[ENDPOINTS.PLAN_WORK_ORDERS]: 10000,
[ENDPOINTS.PLAN_SECTORS]: 15000,
[ENDPOINTS.USER]: 10000,
// Slower endpoints with large datasets
[ENDPOINTS.ITEMS]: 60000, // Items can be large
[ENDPOINTS.SALES]: 60000, // Sales can be large with date ranges
[ENDPOINTS.ITEMS_HIERARCHIES]: 90000, // Hierarchies are the largest
[ENDPOINTS.ITEMS_TREES]: 90000 // Trees can also be large
} as const;
/**
* Retry configuration for different error types
*/
export const RETRY_CONFIG = {
// Network errors - always retry
network: {
enabled: true,
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
backoffMultiplier: 2
},
// Timeout errors - retry with increased timeout
timeout: {
enabled: true,
maxAttempts: 2,
baseDelay: 2000,
maxDelay: 15000,
backoffMultiplier: 2
},
// Server errors (5xx) - limited retry
server: {
enabled: true,
maxAttempts: 2,
baseDelay: 3000,
maxDelay: 20000,
backoffMultiplier: 2
},
// Client errors (4xx) - generally don't retry
client: {
enabled: false,
maxAttempts: 1,
baseDelay: 0,
maxDelay: 0,
backoffMultiplier: 1
}
} as const;
/**
* Header names used by the API
*/
export const HEADERS = {
RESPONSE_TIME: 'x-response-time',
AUTHORIZATION_RESPONSE: 'x-authorization-response',
CONTENT_TYPE: 'content-type'
} as const;
/**
* Authorization response values
*/
export const AUTH_STATUS = {
OK: 'ok',
UNAUTHORIZED: 'unauthorized'
} as const;
/**
* Create a configuration by merging defaults with environment-specific and custom options
*/
export function createConfig(
baseConfig: ClientConfig,
customConfig?: Partial<Omit<ClientConfig, 'baseUrl'>>
): Required<ClientConfig> {
return {
...DEFAULT_CONFIG,
...baseConfig,
...customConfig,
headers: {
...DEFAULT_CONFIG.headers,
...baseConfig.headers,
...customConfig?.headers
}
};
}
/**
* Create a configuration from environment and custom options
*/
export function createConfigFromEnvironment(
environment: string,
customConfig?: Partial<ClientConfig>
): Required<ClientConfig> {
const envConfig = ENVIRONMENT_CONFIGS[environment];
if (!envConfig) {
throw new Error(`Unknown environment: ${environment}. Available environments: ${Object.keys(ENVIRONMENT_CONFIGS).join(', ')}`);
}
return {
...DEFAULT_CONFIG,
...envConfig,
...customConfig,
headers: {
...DEFAULT_CONFIG.headers,
...envConfig.headers,
...customConfig?.headers
}
};
}
/**
* Get timeout for a specific endpoint
*/
export function getEndpointTimeout(endpoint: string, defaultTimeout: number): number {
// Remove path parameters for matching
const normalizedEndpoint = endpoint.replace(/\/\d+/g, '/{id}').replace(/\/[^/]+$/g, '/{param}');
return ENDPOINT_TIMEOUTS[normalizedEndpoint as keyof typeof ENDPOINT_TIMEOUTS] || defaultTimeout;
}
/**
* Validate configuration
*/
export function validateConfig(config: ClientConfig): string[] {
const errors: string[] = [];
if (config.baseUrl && !isValidUrl(config.baseUrl)) {
errors.push('baseUrl must be a valid URL');
}
if (config.timeout && (config.timeout <= 0 || config.timeout > 300000)) {
errors.push('timeout must be between 1ms and 300s (5 minutes)');
}
if (config.retries && (config.retries < 0 || config.retries > 10)) {
errors.push('retries must be between 0 and 10');
}
if (config.retryDelay && (config.retryDelay < 0 || config.retryDelay > 60000)) {
errors.push('retryDelay must be between 0ms and 60s');
}
if (config.logLevel && !['debug', 'info', 'warn', 'error'].includes(config.logLevel)) {
errors.push('logLevel must be one of: debug, info, warn, error');
}
return errors;
}
/**
* Check if a string is a valid URL
*/
function isValidUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
/**
* Get the full API URL for an endpoint
*/
export function getApiUrl(baseUrl: string, endpoint: string): string {
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${cleanBaseUrl}${API_BASE_PATH}${cleanEndpoint}`;
}
/**
* Replace path parameters in endpoint URLs
*/
export function replacePathParams(endpoint: string, params: Record<string, string | number>): string {
let result = endpoint;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`{${key}}`, String(value));
}
return result;
}
/**
* Build query string from parameters
*/
export function buildQueryString(params: Record<string, string | number | boolean | undefined>): string {
const filtered = Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => [key, String(value)]);
if (filtered.length === 0) {
return '';
}
const searchParams = new URLSearchParams();
filtered.forEach(([key, value]) => {
searchParams.append(key, value);
});
return `?${searchParams.toString()}`;
}