npm package: @ritas-inc/hanaqueryapi-client
Description: TypeScript client for HANA Query API with full type safety and error handling
Author: araob
Latest version: 1.0.75

# HANA Query API Client

A fully-featured TypeScript client for the HANA Query API with comprehensive type safety, error handling, retry logic, and logging capabilities.

## ⚠️ Breaking Change in v1.0.0

**Important:** Starting with v1.0.0, the `baseUrl` configuration is now **required** when creating a client instance. The default client has been removed to ensure explicit configuration.

```typescript
// ❌ This no longer works
const client = new HanaQueryClient();

// ✅ This is now required
const client = new HanaQueryClient({ baseUrl: 'http://localhost:3001' });

// ✅ Or use helper functions
const client = createClient({ baseUrl: 'http://localhost:3001' });
const client = createClientFromEnvironment('development');
```

## Features

- 🔷 **Full TypeScript Support** - Complete type definitions for all API responses
- 🔄 **Automatic Retry Logic** - Configurable retry with exponential backoff
- 📝 **Request/Response Logging** - Detailed logging with configurable levels
- 🛡️ **Error Handling** - Custom error types with detailed context
- ⏱️ **Configurable Timeouts** - Per-endpoint timeout configuration
- 🌍 **Multiple Environments** - Built-in support for dev/test/staging/production
- 🔗 **Fluent API** - Optional request builder for complex queries
- ✅ **Type Safety** - Runtime type checking and validation
- 📊 **Header Support** - Access to response timing and authorization headers

## Installation

```bash
npm install @ritas-inc/hanaqueryapi-client
```

> **Automated Publishing**: This package is automatically published to npm when changes are pushed to the master branch. See [NPM Publishing Documentation](../../docs/npm-publishing.md) for details.

### Requirements

- Node.js 24.0.0 or higher (uses native TypeScript support)

### Development

```bash
# Run examples
npm run example:basic
npm run example:advanced
npm run example:errors

# Type checking
npm run typecheck

# Linting
npm run lint
npm run lint:fix
```

## Quick Start

### Basic Usage

```typescript
import { createClient } from '@ritas-inc/hanaqueryapi-client';

// Create client with required baseUrl
const client = createClient({
  baseUrl: 'http://localhost:3001'
});

// Get API health status
const health = await client.getHealth();
console.log('API Status:', health.data.status);

// Get items with date filtering
const items = await client.getItemsStatus({
  from: '2025-01-01',
  to: '2025-12-31'
});
console.log(`Found ${items.metadata.count} items`);

// Get production plans
const plans = await client.getPlans();
for (const plan of plans.data.plans) {
  console.log(`Plan ${plan.plan_id}: ${plan.plan_status}`);
}
```

### Environment-Specific Clients

```typescript
import { clients, createClient } from '@ritas-inc/hanaqueryapi-client';

// Development client (localhost with debug logging)
const devClient = clients.development();

// Production client (production URL with minimal logging)
const prodClient = clients.production();

// Custom client with explicit baseUrl
const customClient = createClient({
  baseUrl: 'https://my-api.example.com'
}, {
  timeout: 60000,
  enableLogging: true,
  logLevel: 'info'
});
```

## API Reference

### Client Methods

#### Health and Documentation

```typescript
// Get API health status
const health = await client.getHealth();
// Response: { success: true, data: { status: "ok", timestamp: "...", uptime: 123 } }

// Get API documentation
const docs = await client.getDocs();
// Response: { success: true, data: { name: "HANA Query API", version: "1.0.0", endpoints: {...} } }
```

#### Items

```typescript
// Get all items status
const allItems = await client.getItemsStatus();

// Get items with date range
const filteredItems = await client.getItemsStatus({
  from: '2025-01-01',
  to: '2025-06-30'
});

// Get item hierarchies
const hierarchies = await client.getItemHierarchies();
```

#### Production Plans

```typescript
// Get all production plans
const plans = await client.getPlans();

// Get specific plan details
const plan = await client.getPlan(1);

// Get plan products
const products = await client.getPlanProducts(1);

// Get plan work orders (may return 404 if no work orders exist)
try {
  const workOrders = await client.getPlanWorkOrders(1);
  console.log(`Plan has ${workOrders.metadata.count} work orders`);
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log('Plan has no work orders');
  }
}

// Get sector summaries for a specific plan
const planSectors = await client.getPlanSectorsSummary(1);
console.log(`Plan has ${planSectors.metadata.sector_count} sectors`);
console.log(`Completion rate: ${planSectors.metadata.completion_rate}%`);

// Get sector summaries for all plans
const allSectors = await client.getAllPlansSectorsSummary();
console.log(`${allSectors.metadata.total_sectors} sectors across ${allSectors.metadata.unique_plans} plans`);
console.log(`Overall completion: ${allSectors.metadata.overall_completion_rate}%`);
```

#### Users

```typescript
// Get user by username
const user = await client.getUser('USER001');
console.log(`User ID: ${user.data.userId}`);
```

### Configuration

#### Basic Configuration

```typescript
import { HanaQueryClient } from '@company/hana-query-client';

const client = new HanaQueryClient({
  baseUrl: 'https://api.example.com',
  timeout: 30000,        // 30 seconds
  retries: 3,            // Retry up to 3 times
  retryDelay: 1000,      // 1 second base delay
  enableLogging: true,   // Enable request logging
  logLevel: 'info',      // Log level: debug, info, warn, error
  headers: {             // Additional headers
    'Custom-Header': 'value'
  }
});
```

#### Environment Configurations

```typescript
// Built-in environment configurations
const configs = {
  development: {
    baseUrl: 'http://localhost:3001',
    enableLogging: true,
    logLevel: 'debug',
    timeout: 10000
  },
  production: {
    baseUrl: 'https://api.example.com',
    enableLogging: false,
    logLevel: 'error',
    timeout: 30000
  }
};

// Use environment configuration
const client = new HanaQueryClient({}, 'development');
```

### Error Handling

The client provides detailed error types for different scenarios:

```typescript
import { 
  NetworkError, 
  TimeoutError, 
  ValidationError, 
  AuthorizationError, 
  NotFoundError, 
  ServerError 
} from '@company/hana-query-client';

try {
  const items = await client.getItemsStatus();
} catch (error) {
  if (error instanceof NetworkError) {
    console.error('Network connection failed:', error.message);
  } else if (error instanceof TimeoutError) {
    console.error('Request timed out after', error.context?.duration, 'ms');
  } else if (error instanceof ValidationError) {
    console.error('Invalid parameters:', error.message);
  } else if (error instanceof NotFoundError) {
    console.error('Resource not found:', error.message);
  } else if (error instanceof ServerError) {
    console.error('Server error:', error.statusCode, error.message);
  }
}
```

### Fluent Request Builder API

For advanced usage, you can use the fluent request builder:

```typescript
// Complex request with custom options
const items = await client
  .request('/items/status')
  .query({ from: '2025-01-01', to: '2025-06-30' })
  .timeout(60000)
  .retries(5)
  .execute();

// With abort signal
const controller = new AbortController();
const promise = client
  .request('/plans')
  .signal(controller.signal)
  .execute();

// Cancel the request after 5 seconds
setTimeout(() => controller.abort(), 5000);
```

### Utility Functions

#### Convenience Functions

```typescript
import { createClient, utils } from '@ritas-inc/hanaqueryapi-client';

// Create a client instance (required for utility functions)
const client = createClient({ baseUrl: 'http://localhost:3001' });

// Test API connection
const isOnline = await utils.testConnection(client);

// Get all plan IDs
const planIds = await utils.getPlanIds(client);

// Get complete plan data (plan + products + work orders)
const completePlan = await utils.getCompletePlanData(client, 1);
console.log({
  plan: completePlan.plan,
  products: completePlan.products,
  workOrders: completePlan.workOrders,
  errors: completePlan.errors // Any errors that occurred
});

// Search items by keyword
const searchResults = await utils.searchItems(client, 'pump');

// Get low stock items
const lowStock = await utils.getLowStockItems(client);
```

#### Static Endpoint Functions

```typescript
// Use endpoints with a client instance
import { createClient, endpoints } from '@ritas-inc/hanaqueryapi-client';

const client = createClient({ baseUrl: 'http://localhost:3001' });

const health = await endpoints.health(client);
const plans = await endpoints.plans(client);
const items = await endpoints.itemsStatus(client, { from: '2025-01-01' });
```

## TypeScript Integration

### Type-Safe Responses

All API responses are fully typed:

```typescript
import type { 
  ItemsStatusResponse, 
  PlansResponse, 
  Plan,
  ItemStatus 
} from '@ritas-inc/hanaqueryapi-client';

const items: ItemsStatusResponse = await client.getItemsStatus();

// TypeScript knows the exact structure
items.data.items.forEach((item: ItemStatus) => {
  console.log(`${item.itemcode}: ${item.onhand} in stock`);
});

const plans: PlansResponse = await client.getPlans();
plans.data.plans.forEach((plan: Plan) => {
  console.log(`Plan ${plan.plan_id} created by ${plan.plan_username}`);
});
```

### Type Guards

```typescript
import { isSuccessResponse, isErrorResponse } from '@ritas-inc/hanaqueryapi-client';

const response = await client.getPlans();

if (isSuccessResponse(response)) {
  // TypeScript knows this is a success response
  console.log(response.data.plans);
} else if (isErrorResponse(response)) {
  // TypeScript knows this is an error response
  console.error(response.problem.detail);
}
```

## Advanced Examples

### Complete Production Planning Workflow

```typescript
import { clients, utils, isNotFoundError } from '@ritas-inc/hanaqueryapi-client';

async function analyzeProductionPlanning() {
  const client = clients.production();
  
  try {
    // 1. Check API health
    const health = await client.getHealth();
    console.log(`API is ${health.data.status}, uptime: ${health.data.uptime}s`);
    
    // 2. Get all production plans
    const plans = await client.getPlans();
    console.log(`Found ${plans.metadata.count} production plans`);
    
    // 3. Analyze each plan
    for (const plan of plans.data.plans) {
      console.log(`\\nAnalyzing Plan ${plan.plan_id}:`);
      console.log(`  Status: ${plan.plan_status}`);
      console.log(`  Created by: ${plan.plan_username}`);
      console.log(`  Products: ${plan.products_total} total, ${plan.products_released} released`);
      
      // 4. Get plan details
      try {
        const products = await client.getPlanProducts(plan.plan_id);
        console.log(`  Product details: ${products.metadata.count} items`);
        
        const workOrders = await client.getPlanWorkOrders(plan.plan_id);
        console.log(`  Work orders: ${workOrders.metadata.count} orders`);
        
        // Calculate completion rate
        const totalPlanned = workOrders.data.workOrders.reduce(
          (sum, order) => sum + order.order_plannedqty, 0
        );
        const totalCompleted = workOrders.data.workOrders.reduce(
          (sum, order) => sum + order.order_completedqty, 0
        );
        const completionRate = totalPlanned > 0 ? (totalCompleted / totalPlanned * 100) : 0;
        
        console.log(`  Completion rate: ${completionRate.toFixed(1)}%`);
        
      } catch (error) {
        if (isNotFoundError(error)) {
          console.log(`  No additional data available for plan ${plan.plan_id}`);
        } else {
          console.error(`  Error fetching plan details:`, error.message);
        }
      }
    }
    
    // 5. Get low stock items that need attention
    const lowStockItems = await utils.getLowStockItems(client);
    if (lowStockItems.length > 0) {
      console.log(`\\n⚠️  Low stock items requiring attention:`);
      lowStockItems.forEach(item => {
        console.log(`  ${item.itemcode}: ${item.onhand}/${item.min} (${item.description})`);
      });
    }
    
  } catch (error) {
    console.error('Production planning analysis failed:', error);
  }
}

analyzeProductionPlanning();
```

### Custom Error Handling Strategy

```typescript
import { 
  HanaQueryClient, 
  HanaQueryClientError,
  isRetryableError,
  getRetryDelay 
} from '@company/hana-query-client';

class RobustAPIClient {
  private client: HanaQueryClient;
  
  constructor() {
    this.client = new HanaQueryClient({
      enableLogging: true,
      logLevel: 'info'
    });
  }
  
  async safeRequest<T>(operation: () => Promise<T>, maxRetries = 3): Promise<T | null> {
    let lastError: HanaQueryClientError | null = null;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        if (error instanceof HanaQueryClientError) {
          lastError = error;
          
          console.warn(`Attempt ${attempt} failed:`, {
            type: error.type,
            message: error.message,
            statusCode: error.statusCode
          });
          
          // Don't retry non-retryable errors
          if (!isRetryableError(error)) {
            break;
          }
          
          // Wait before retrying
          if (attempt < maxRetries) {
            const delay = getRetryDelay(attempt, 1000, 10000);
            console.log(`Waiting ${delay}ms before retry...`);
            await new Promise(resolve => setTimeout(resolve, delay));
          }
        } else {
          // Unexpected error
          console.error('Unexpected error:', error);
          break;
        }
      }
    }
    
    console.error('All retry attempts failed:', lastError?.message);
    return null;
  }
  
  async getPlansWithFallback() {
    const plans = await this.safeRequest(() => this.client.getPlans());
    return plans?.data.plans || [];
  }
}
```

## Best Practices

### 1. Environment Configuration

**Important:** This client library does NOT access environment variables directly. Environment handling should be done by the consuming application.

```typescript
// ✅ Good: Let your application handle environment detection
const getClientConfig = () => {
  const isDevelopment = process.env.NODE_ENV === 'development';
  const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3001';
  
  return {
    baseUrl: apiBaseUrl,
    enableLogging: isDevelopment,
    logLevel: isDevelopment ? 'debug' as const : 'error' as const,
    timeout: isDevelopment ? 10000 : 30000
  };
};

const client = createClient(getClientConfig());

// ✅ Or use environment-specific presets
const client = process.env.NODE_ENV === 'production' 
  ? clients.production()
  : clients.development();

// ❌ Bad: The client doesn't read environment variables itself
// This ensures your library consumers have full control over configuration
```

### 2. Error Handling

```typescript
// Always handle specific error types
try {
  const data = await client.getItemsStatus();
} catch (error) {
  if (isNotFoundError(error)) {
    // Handle gracefully - this might be expected
    return [];
  } else if (isNetworkError(error) || isTimeoutError(error)) {
    // Retry or show user-friendly message
    throw new Error('Unable to connect to server. Please try again.');
  } else {
    // Log unexpected errors
    console.error('Unexpected API error:', error);
    throw error;
  }
}
```

### 3. Performance Optimization

```typescript
// Use appropriate timeouts for different endpoints
const client = new HanaQueryClient({
  timeout: 30000  // Default timeout
});

// Override for slow endpoints
const hierarchies = await client.getItemHierarchies({
  timeout: 90000  // Longer timeout for large dataset
});

// Use parallel requests when possible
const [plans, items] = await Promise.all([
  client.getPlans(),
  client.getItemsStatus()
]);
```

### 4. Request Cancellation

```typescript
// Cancel long-running requests
const controller = new AbortController();

const promise = client.getItemHierarchies({
  signal: controller.signal
});

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30000);

try {
  const result = await promise;
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled');
  }
}
```

## Contributing

To contribute to this client:

1. Clone the repository
2. Install dependencies: `npm install`
3. Make your changes
4. Run tests: `npm test`
5. Build: `npm run build`
6. Submit a pull request