@ritas-inc/hanaqueryapi-client
Version:
TypeScript client for HANA Query API with full type safety and error handling
901 lines (819 loc) • 30.5 kB
text/typescript
/**
* HANA Query API Client - Main Client Class
*
* Complete TypeScript client for the HANA Query API with full type safety,
* error handling, retry logic, and logging.
*/
import type {
ClientConfig,
RequestOptions,
RequestContext,
SalesParams,
HealthData,
HealthMetadata,
DocsData,
DocsMetadata,
ItemsStatusData,
ItemsStatusMetadata,
ItemGroupsData,
ItemGroupsMetadata,
SalesData,
SalesMetadata,
HierarchiesData,
HierarchiesMetadata,
TreesData,
TreesMetadata,
ProductionSectorsData,
ProductionSectorsMetadata,
PlansData,
PlansMetadata,
SinglePlanData,
SinglePlanMetadata,
PlanProductsData,
PlanProductsMetadata,
PlanWorkOrdersData,
PlanWorkOrdersMetadata,
PlanSectorsSummaryData,
PlanSectorsSummaryMetadata,
AllPlansSectorsSummaryData,
AllPlansSectorsSummaryMetadata,
PlanSectorSummary,
UserData,
UserMetadata,
APIResponse,
APIResponseHeaders,
SuccessResponse
} from './types.ts';
import {
createConfig,
createConfigFromEnvironment,
getApiUrl,
replacePathParams,
buildQueryString,
getEndpointTimeout,
ENDPOINTS,
HEADERS,
AUTH_STATUS,
RETRY_CONFIG
} from './config.ts';
import {
HanaQueryClientError,
AuthorizationError,
NotFoundError,
createErrorFromResponse,
createNetworkError,
isRetryableError,
getRetryDelay
} from './errors.ts';
/**
* Main HANA Query API Client
*/
export class HanaQueryClient {
private readonly config: Required<ClientConfig>;
constructor(config: ClientConfig, customConfig?: Partial<Omit<ClientConfig, 'baseUrl'>>) {
this.config = createConfig(config, customConfig);
this.log('debug', 'Client initialized', { config: this.config });
}
// =========================================================================
// Public API Methods
// =========================================================================
/**
* Get API health status with system information
*
* @param options - Optional request configuration
* @returns Promise resolving to health data and system metadata
*
* @example
* ```typescript
* const { data, metadata } = await client.getHealth();
* console.log(`API Status: ${data.status}`);
* console.log(`Uptime: ${Math.round(data.uptime / 60)} minutes`);
* console.log(`Node Version: ${metadata.nodeVersion}`);
* console.log(`Platform: ${metadata.platform}`);
* ```
*
* @returns {Object} result - Health status result
* @returns {Object} result.data - Health status data
* @returns {string} result.data.status - Always 'ok' when API is running
* @returns {string} result.data.timestamp - Current timestamp in ISO 8601 format
* @returns {number} result.data.uptime - Server uptime in seconds
* @returns {Object} result.metadata - System information metadata
* @returns {boolean} result.metadata.systemInfo - Indicates system info is included
* @returns {string} result.metadata.serverTime - Server current time
* @returns {string} result.metadata.nodeVersion - Node.js version
* @returns {string} result.metadata.platform - Operating system platform
* @returns {string} result.metadata.arch - System architecture
*/
async getHealth(options?: RequestOptions): Promise<{ data: HealthData; metadata: HealthMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<HealthData, HealthMetadata>>(ENDPOINTS.HEALTH, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get API documentation with endpoint information
*
* @param options - Optional request configuration
* @returns Promise resolving to API documentation and metadata
*
* @example
* ```typescript
* const { data, metadata } = await client.getDocs();
* console.log(`API: ${data.name} v${data.version}`);
* console.log(`Available endpoints: ${metadata.endpointCount}`);
* console.log('Endpoints:', Object.keys(data.endpoints));
* ```
*
* @returns {Object} result - API documentation result
* @returns {Object} result.data - API documentation data
* @returns {string} result.data.name - API name
* @returns {string} result.data.version - API version
* @returns {Object} result.data.endpoints - Map of endpoint paths to descriptions
* @returns {Object} result.metadata - Documentation metadata
* @returns {number} result.metadata.endpointCount - Total number of available endpoints
* @returns {string} result.metadata.apiVersion - API version identifier
* @returns {string} result.metadata.generatedAt - Documentation generation timestamp
* @returns {boolean} result.metadata.documentation - Indicates this is documentation endpoint
*/
async getDocs(options?: RequestOptions): Promise<{ data: DocsData; metadata: DocsMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<DocsData, DocsMetadata>>(ENDPOINTS.DOCS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get all items without date filtering
*/
async getItems(options?: RequestOptions): Promise<{ data: ItemsStatusData; metadata: ItemsStatusMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<ItemsStatusData, ItemsStatusMetadata>>(ENDPOINTS.ITEMS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get all item groups
*
* Retrieves all item groups from the SAP HANA database with their codes and names.
* Item groups provide categorization information for inventory items.
*
* @param {RequestOptions} [options] - Optional request configuration
* @returns {Promise<{data: ItemGroupsData, metadata: ItemGroupsMetadata}>} Item groups with metadata
*
* @example
* ```typescript
* const response = await client.getItemGroups();
* console.log(`Found ${response.metadata.count} item groups`);
* response.data.groups.forEach(group => {
* console.log(`${group.code}: ${group.name}`);
* });
* ```
*/
async getItemGroups(options?: RequestOptions): Promise<{ data: ItemGroupsData; metadata: ItemGroupsMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<ItemGroupsData, ItemGroupsMetadata>>(ENDPOINTS.ITEMS_GROUPS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get sales data within a date range
*/
async getSales(
params: SalesParams,
options?: RequestOptions
): Promise<{ data: SalesData; metadata: SalesMetadata }> {
const endpoint = ENDPOINTS.SALES + buildQueryString(params as unknown as Record<string, string | number | boolean | undefined>);
const response = await this.executeAPIRequest<SuccessResponse<SalesData, SalesMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get all item hierarchies
*/
async getItemHierarchies(options?: RequestOptions): Promise<{ data: HierarchiesData; metadata: HierarchiesMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<HierarchiesData, HierarchiesMetadata>>(ENDPOINTS.ITEMS_HIERARCHIES, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get raw tree relationships (parent-child-quantity)
*/
async getItemTrees(options?: RequestOptions): Promise<{ data: TreesData; metadata: TreesMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<TreesData, TreesMetadata>>(ENDPOINTS.ITEMS_TREES, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get all production sectors
*/
async getProductionSectors(options?: RequestOptions): Promise<{ data: ProductionSectorsData; metadata: ProductionSectorsMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<ProductionSectorsData, ProductionSectorsMetadata>>(ENDPOINTS.PRODUCTION_SECTORS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get all production plans
*/
async getPlans(options?: RequestOptions): Promise<{ data: PlansData; metadata: PlansMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<PlansData, PlansMetadata>>(ENDPOINTS.PLANS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get a specific production plan by ID
*/
async getPlan(planId: number | string, options?: RequestOptions): Promise<{ data: SinglePlanData; metadata: SinglePlanMetadata }> {
const endpoint = replacePathParams(ENDPOINTS.PLAN_DETAIL, { planId });
const response = await this.executeAPIRequest<SuccessResponse<SinglePlanData, SinglePlanMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get products for a specific production plan
*
* @param planId - The plan ID to get products for
* @param options - Optional request configuration
* @returns Promise resolving to plan products data and metadata
*
* @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
* @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
* @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
* @throws {HanaQueryClientError} For other client or server errors
*
* @example
* ```typescript
* try {
* const result = await client.getPlanProducts(123);
* if (result.data.products.length === 0) {
* console.log('Plan exists but has no products');
* } else {
* console.log(`Found ${result.data.products.length} products`);
* }
* } catch (error) {
* if (error instanceof NotFoundError) {
* console.log('Plan does not exist');
* } else {
* console.log('Other error:', error.message);
* }
* }
* ```
*
* @remarks
* - Returns HTTP 200 with empty array when plan exists but has no products
* - Returns HTTP 404 only when the plan itself doesn't exist
* - Empty products array is a normal, successful response
*/
async getPlanProducts(
planId: number | string,
options?: RequestOptions
): Promise<{ data: PlanProductsData; metadata: PlanProductsMetadata }> {
const endpoint = replacePathParams(ENDPOINTS.PLAN_PRODUCTS, { planId });
const response = await this.executeAPIRequest<SuccessResponse<PlanProductsData, PlanProductsMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get work orders for a specific production plan
*
* @param planId - The plan ID to get work orders for
* @param options - Optional request configuration
* @returns Promise resolving to plan work orders data and metadata
*
* @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
* @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
* @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
* @throws {HanaQueryClientError} For other client or server errors
*
* @example
* ```typescript
* try {
* const result = await client.getPlanWorkOrders(123);
* if (result.data.workOrders.length === 0) {
* console.log('Plan exists but has no work orders');
* } else {
* console.log(`Found ${result.data.workOrders.length} work orders`);
* }
* } catch (error) {
* if (error instanceof NotFoundError) {
* console.log('Plan does not exist');
* } else {
* console.log('Other error:', error.message);
* }
* }
* ```
*
* @remarks
* - Returns HTTP 200 with empty array when plan exists but has no work orders
* - Returns HTTP 404 only when the plan itself doesn't exist
* - Empty work orders array is a normal, successful response
*/
async getPlanWorkOrders(
planId: number | string,
options?: RequestOptions
): Promise<{ data: PlanWorkOrdersData; metadata: PlanWorkOrdersMetadata }> {
const endpoint = replacePathParams(ENDPOINTS.PLAN_WORK_ORDERS, { planId });
const response = await this.executeAPIRequest<SuccessResponse<PlanWorkOrdersData, PlanWorkOrdersMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get sector summaries for a specific production plan
*
* @param planId - The plan ID to get sector summaries for
* @param options - Optional request configuration
* @returns Promise resolving to plan sectors summary data and metadata
*
* @throws {NotFoundError} When the plan with the specified ID doesn't exist (HTTP 404)
* @throws {ValidationError} When the planId parameter is invalid (HTTP 400)
* @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
* @throws {HanaQueryClientError} For other client or server errors
*
* @example
* ```typescript
* try {
* const result = await client.getPlanSectorsSummary(123);
* if (result.data.sectors.length === 0) {
* console.log('Plan exists but has no sectors');
* } else {
* console.log(`Found ${result.data.sectors.length} sectors`);
* console.log(`Completion rate: ${result.metadata.completion_rate}%`);
* }
* } catch (error) {
* if (error instanceof NotFoundError) {
* console.log('Plan does not exist');
* } else {
* console.log('Other error:', error.message);
* }
* }
* ```
*
* @remarks
* - Returns HTTP 200 with empty array when plan exists but has no sectors
* - Returns HTTP 404 only when the plan itself doesn't exist
* - Empty sectors array is a normal, successful response
* - Metadata includes aggregated statistics (total planned, completed, completion rate)
*/
async getPlanSectorsSummary(
planId: number | string,
options?: RequestOptions
): Promise<{ data: PlanSectorsSummaryData; metadata: PlanSectorsSummaryMetadata }> {
const endpoint = replacePathParams(ENDPOINTS.PLAN_SECTORS, { planId });
const response = await this.executeAPIRequest<SuccessResponse<PlanSectorsSummaryData, PlanSectorsSummaryMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get sector summaries for all production plans
*
* @param options - Optional request configuration
* @returns Promise resolving to all plans sectors summary data and metadata
*
* @throws {ValidationError} When the request parameters are invalid (HTTP 400)
* @throws {AuthorizationError} When the request is not authorized (HTTP 401/403)
* @throws {HanaQueryClientError} For other client or server errors
*
* @example
* ```typescript
* const result = await client.getAllPlansSectorsSummary();
* console.log(`Found ${result.data.sectors.length} total sectors`);
* console.log(`Across ${result.metadata.unique_plans} unique plans`);
* console.log(`Overall completion rate: ${result.metadata.overall_completion_rate}%`);
*
* // Group sectors by plan
* const sectorsByPlan = result.data.sectors.reduce((acc, sector) => {
* if (!acc[sector.plan_entry]) acc[sector.plan_entry] = [];
* acc[sector.plan_entry].push(sector);
* return acc;
* }, {} as Record<number, PlanSectorSummary[]>);
* ```
*
* @remarks
* - Returns all sector summaries across all plans in the system
* - Empty array indicates no sectors exist in any plan
* - Metadata includes overall statistics and unique counts
*/
async getAllPlansSectorsSummary(
options?: RequestOptions
): Promise<{ data: AllPlansSectorsSummaryData; metadata: AllPlansSectorsSummaryMetadata }> {
const response = await this.executeAPIRequest<SuccessResponse<AllPlansSectorsSummaryData, AllPlansSectorsSummaryMetadata>>(ENDPOINTS.ALL_PLANS_SECTORS, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
/**
* Get user by username
*/
async getUser(username: string, options?: RequestOptions): Promise<{ data: UserData; metadata: UserMetadata }> {
const endpoint = replacePathParams(ENDPOINTS.USER, { username: encodeURIComponent(username) });
const response = await this.executeAPIRequest<SuccessResponse<UserData, UserMetadata>>(endpoint, undefined, options);
return { data: response.data, metadata: response.metadata! };
}
// =========================================================================
// Convenience Methods
// =========================================================================
/**
* Check if a plan exists without fetching its full details
*
* @param planId - The plan ID to check
* @param options - Optional request configuration
* @returns Promise resolving to true if plan exists, false otherwise
*
* @example
* ```typescript
* const exists = await client.planExists(123);
* if (exists) {
* console.log('Plan exists');
* } else {
* console.log('Plan not found');
* }
* ```
*/
async planExists(planId: number | string, options?: RequestOptions): Promise<boolean> {
try {
await this.getPlan(planId, options);
return true;
} catch (error) {
if (error instanceof NotFoundError) {
return false;
}
throw error;
}
}
/**
* Get plan products with explicit plan existence information
*
* @param planId - The plan ID to get products for
* @param options - Optional request configuration
* @returns Promise resolving to plan existence status, products, and metadata
*
* @example
* ```typescript
* const result = await client.getPlanProductsSafe(123);
* if (!result.planExists) {
* console.log('Plan does not exist');
* } else if (result.products.length === 0) {
* console.log('Plan exists but has no products');
* } else {
* console.log(`Plan has ${result.products.length} products`);
* }
* ```
*/
async getPlanProductsSafe(
planId: number | string,
options?: RequestOptions
): Promise<{
planExists: boolean;
products: PlanProductsData['products'];
metadata: PlanProductsMetadata;
}> {
try {
const result = await this.getPlanProducts(planId, options);
return {
planExists: true,
products: result.data.products,
metadata: result.metadata
};
} catch (error) {
if (error instanceof NotFoundError) {
return {
planExists: false,
products: [],
metadata: { count: 0, planId: Number(planId) }
};
}
throw error;
}
}
/**
* Get plan work orders with explicit plan existence information
*
* @param planId - The plan ID to get work orders for
* @param options - Optional request configuration
* @returns Promise resolving to plan existence status, work orders, and metadata
*
* @example
* ```typescript
* const result = await client.getPlanWorkOrdersSafe(123);
* if (!result.planExists) {
* console.log('Plan does not exist');
* } else if (result.workOrders.length === 0) {
* console.log('Plan exists but has no work orders');
* } else {
* console.log(`Plan has ${result.workOrders.length} work orders`);
* }
* ```
*/
async getPlanWorkOrdersSafe(
planId: number | string,
options?: RequestOptions
): Promise<{
planExists: boolean;
workOrders: PlanWorkOrdersData['workOrders'];
metadata: PlanWorkOrdersMetadata;
}> {
try {
const result = await this.getPlanWorkOrders(planId, options);
return {
planExists: true,
workOrders: result.data.workOrders,
metadata: result.metadata
};
} catch (error) {
if (error instanceof NotFoundError) {
return {
planExists: false,
workOrders: [],
metadata: { count: 0, planId: Number(planId) }
};
}
throw error;
}
}
// =========================================================================
// Request Builder API (Fluent Interface)
// =========================================================================
/**
* Create a request builder for fluent API usage
*/
request(endpoint: string): RequestBuilder {
return new RequestBuilder(this, endpoint);
}
// =========================================================================
// Core Request Method
// =========================================================================
/**
* Core request method with retry logic and error handling
*/
private async executeAPIRequest<T extends APIResponse>(
endpoint: string,
body?: any,
options?: RequestOptions
): Promise<T> {
const mergedOptions = {
timeout: getEndpointTimeout(endpoint, this.config.timeout),
retries: this.config.retries,
...options
};
let lastError: HanaQueryClientError | undefined;
for (let attempt = 1; attempt <= mergedOptions.retries + 1; attempt++) {
const context: RequestContext = {
method: 'GET',
url: endpoint,
startTime: Date.now(),
attempt,
maxAttempts: mergedOptions.retries + 1
};
try {
this.log('debug', `Request attempt ${attempt}`, { endpoint, attempt });
const result = await this.executeRequest<T>(endpoint, body, mergedOptions, context);
this.log('info', 'Request successful', {
endpoint,
attempt,
duration: context.duration
});
return result;
} catch (error) {
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
if (error instanceof HanaQueryClientError) {
lastError = error;
} else {
lastError = createNetworkError(error as Error, context);
}
this.log('warn', `Request attempt ${attempt} failed`, {
endpoint,
attempt,
error: lastError.message,
duration: context.duration
});
// Don't retry if this is the last attempt or error is not retryable
if (attempt >= mergedOptions.retries + 1 || !isRetryableError(lastError)) {
break;
}
// Calculate delay and wait before retrying
const delay = getRetryDelay(attempt, this.config.retryDelay, 30000);
this.log('debug', `Retrying in ${delay}ms`, { endpoint, attempt, delay });
await this.sleep(delay);
}
}
this.log('error', 'Request failed after all retries', {
endpoint,
attempts: mergedOptions.retries + 1,
error: lastError?.message
});
throw lastError!;
}
/**
* Execute a single HTTP request
*/
private async executeRequest<T extends APIResponse>(
endpoint: string,
body: any,
options: RequestOptions & { timeout: number },
context: RequestContext
): Promise<T> {
const url = getApiUrl(this.config.baseUrl, endpoint);
context.url = url;
// Setup request options
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
// Merge signal if provided
if (options.signal) {
options.signal.addEventListener('abort', () => controller.abort());
}
const requestInit: RequestInit = {
method: 'GET',
headers: {
...this.config.headers
},
signal: controller.signal
};
// Add body for POST/PUT requests (currently API only uses GET)
if (body && requestInit.method !== 'GET') {
requestInit.body = JSON.stringify(body);
}
let response: Response;
try {
this.log('debug', 'Sending HTTP request', { url, method: requestInit.method });
response = await fetch(url, requestInit);
} catch (error) {
throw createNetworkError(error as Error, context);
} finally {
clearTimeout(timeoutId);
context.endTime = Date.now();
context.duration = context.endTime - context.startTime;
}
// Parse response headers
const headers = this.parseResponseHeaders(response);
// Log response info
this.log('debug', 'Received HTTP response', {
url,
status: response.status,
headers,
duration: context.duration
});
// Check authorization header
this.checkAuthorization(headers, context);
// Parse response body
let responseData: any;
try {
const responseText = await response.text();
responseData = responseText ? JSON.parse(responseText) : null;
} catch (error) {
throw createNetworkError(
new Error(`Failed to parse response JSON: ${(error as Error).message}`),
context
);
}
// Handle HTTP errors
if (!response.ok) {
throw createErrorFromResponse(response, responseData, context);
}
// Handle API errors (success: false)
if (responseData && responseData.success === false) {
throw createErrorFromResponse(response, responseData, context);
}
// Validate response structure
if (!responseData || typeof responseData.success !== 'boolean') {
throw createNetworkError(
new Error('Invalid API response format'),
context
);
}
return responseData as T;
}
// =========================================================================
// Helper Methods
// =========================================================================
/**
* Parse response headers into typed object
*/
private parseResponseHeaders(response: Response): APIResponseHeaders {
const responseTime = response.headers.get(HEADERS.RESPONSE_TIME);
const authResponse = response.headers.get(HEADERS.AUTHORIZATION_RESPONSE);
const contentType = response.headers.get(HEADERS.CONTENT_TYPE);
return {
'x-response-time': responseTime || undefined,
'x-authorization-response': (authResponse as 'ok' | 'unauthorized') || undefined,
'content-type': contentType || undefined
};
}
/**
* Check authorization status from headers
*/
private checkAuthorization(headers: APIResponseHeaders, context: RequestContext): void {
const authStatus = headers['x-authorization-response'];
if (authStatus === AUTH_STATUS.UNAUTHORIZED) {
throw new AuthorizationError(
'Request unauthorized',
401,
context
);
}
if (authStatus && authStatus !== AUTH_STATUS.OK) {
this.log('warn', `Unknown authorization status: ${authStatus}`, { context });
}
}
/**
* Sleep for specified milliseconds
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Logging method
*/
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: any): void {
if (!this.config.enableLogging) {
return;
}
const logLevels = ['debug', 'info', 'warn', 'error'];
const configLevel = logLevels.indexOf(this.config.logLevel);
const messageLevel = logLevels.indexOf(level);
if (messageLevel < configLevel) {
return;
}
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [HanaQueryClient] [${level.toUpperCase()}]`;
if (data) {
console[level === 'debug' ? 'log' : level](`${prefix} ${message}`, data);
} else {
console[level === 'debug' ? 'log' : level](`${prefix} ${message}`);
}
}
// =========================================================================
// Configuration and Utilities
// =========================================================================
/**
* Get current client configuration
*/
getConfig(): Readonly<Required<ClientConfig>> {
return { ...this.config };
}
/**
* Update client configuration
*/
updateConfig(updates: Partial<ClientConfig>): void {
Object.assign(this.config, updates);
this.log('debug', 'Configuration updated', { updates });
}
/**
* Test connection to the API
*/
async testConnection(): Promise<boolean> {
try {
await this.getHealth({ timeout: 5000, retries: 1 });
return true;
} catch {
return false;
}
}
}
/**
* Request Builder for fluent API usage
*/
export class RequestBuilder {
private client: HanaQueryClient;
private endpoint: string;
private params: Record<string, any> = {};
private requestOptions: RequestOptions = {};
constructor(client: HanaQueryClient, endpoint: string) {
this.client = client;
this.endpoint = endpoint;
}
/**
* Add query parameters
*/
query(params: Record<string, any>): this {
this.params = { ...this.params, ...params };
return this;
}
/**
* Set request timeout
*/
timeout(ms: number): this {
this.requestOptions.timeout = ms;
return this;
}
/**
* Set retry count
*/
retries(count: number): this {
this.requestOptions.retries = count;
return this;
}
/**
* Set abort signal
*/
signal(signal: AbortSignal): this {
this.requestOptions.signal = signal;
return this;
}
/**
* Execute the request
*/
async execute<T extends APIResponse>(): Promise<T> {
const finalEndpoint = this.endpoint + buildQueryString(this.params);
// Access the private method through the prototype
const clientMethod = (this.client as any).executeAPIRequest;
return clientMethod.call(this.client, finalEndpoint, undefined, this.requestOptions);
}
}
/**
* Factory function to create a client instance
*/
export function createClient(config: ClientConfig, customConfig?: Partial<Omit<ClientConfig, 'baseUrl'>>): HanaQueryClient {
return new HanaQueryClient(config, customConfig);
}
/**
* Factory function to create a client from environment configuration
*/
export function createClientFromEnvironment(environment: string, customConfig?: Partial<ClientConfig>): HanaQueryClient {
const envConfig = createConfigFromEnvironment(environment, customConfig);
return new HanaQueryClient(envConfig);
}
/**
* Default client instance (requires baseUrl to be set via environment or manual configuration)
* Example: createClient({ baseUrl: 'http://localhost:3001' })
*/
// export const defaultClient = createClient({ baseUrl: 'http://localhost:3001' });