# @ritas-inc/hanaqueryapi-client

Typed TypeScript client for the HANA Query API. Wraps every `GET /api/v1/*` endpoint with full type definitions, configurable retries and timeouts, custom error classes, request cancellation, and a fluent request builder.

> **Looking for the HTTP contract?** This README documents the npm package. For the underlying HTTP API (response shapes, search semantics, error envelope, etc.) see [`HANAQUERYAPI_CLIENT_MANUAL.md`](../../HANAQUERYAPI_CLIENT_MANUAL.md) at the repo root.

## Table of contents

1. [Install](#install)
2. [Quick start](#quick-start)
3. [Configuration](#configuration)
4. [Method reference](#method-reference)
5. [Search (business partners & contacts)](#search-business-partners--contacts)
6. [Request options & cancellation](#request-options--cancellation)
7. [Error handling](#error-handling)
8. [Request builder (fluent API)](#request-builder-fluent-api)
9. [TypeScript types](#typescript-types)
10. [Recipes](#recipes)

## Install

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

### Requirements

- Node.js **≥ 24.0.0** (uses native TypeScript / `--experimental-strip-types`).
- TypeScript **≥ 5** if you want compile-time types (the package ships both source `.ts` and built `.d.ts`).

### Breaking change since v1.0.0

`baseUrl` is **required** when constructing the client. The zero-arg default constructor was removed.

```typescript
// No longer supported:
const client = new HanaQueryClient();

// Supported — pick one:
const client = new HanaQueryClient({ baseUrl: 'http://<host>:3001' });
const client = createClient({ baseUrl: 'http://<host>:3001' });
const client = createClientFromEnvironment('development');
```

## Quick start

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

const client = new HanaQueryClient({ baseUrl: 'http://<host>:3001' });

// Health
const { data, metadata } = await client.getHealth();
console.log(`API ${data.status}, uptime ${data.uptime}s`);

// Production plans
const plans = await client.getPlans();
for (const p of plans.data.plans) {
  console.log(p.plan_id, p.plan_status);
}

// Items, optionally filtered by group codes
const items = await client.getItems({ groups: [131, 144] });
console.log(`${items.metadata.count} items`);

// Sales window
const sales = await client.getSales({ from: '2026-01-01', to: '2026-03-31' });

// Business-partner fuzzy search
const matches = await client.searchBusinessPartners({ q: 'jose silva', fuzziness: 0.7 });
console.log(`${matches.metadata.count} matches`);
```

Every method returns `{ data, metadata }`. Errors throw typed classes (see [§7](#error-handling)).

## Configuration

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

const client = new HanaQueryClient({
  baseUrl: 'http://<host>:3001',  // required
  timeout: 30000,                  // ms — default 30000
  retries: 3,                      // attempts on retryable errors — default 3
  retryDelay: 1000,                // ms base; exponential backoff applied
  enableLogging: false,            // default false
  logLevel: 'info',                // 'debug' | 'info' | 'warn' | 'error'
  headers: { 'X-Trace-Id': '...' } // additional headers sent on every request
});
```

### Environment presets

`createClientFromEnvironment(name)` builds a client with sensible defaults per environment:

| Preset | baseUrl | logging | timeout | retries |
|---|---|---|---|---|
| `development` | `http://localhost:3001` | `debug` | 10s | 3 |
| `testing` | `http://localhost:3001` | `warn`  | 5s  | 1 |
| `staging` | `https://api-staging.example.com` | `info`  | 20s | 3 |
| `production` | `https://api.example.com` | `error` | 30s | 3 |

```typescript
import { createClientFromEnvironment } from '@ritas-inc/hanaqueryapi-client';
const client = createClientFromEnvironment('development');
```

Override individual fields:

```typescript
import { createClient } from '@ritas-inc/hanaqueryapi-client';
const client = createClient({
  baseUrl: 'https://my-api.example.com'
}, {
  timeout: 60000,
  enableLogging: true,
  logLevel: 'info'
});
```

### Per-endpoint timeouts

The client automatically applies a longer timeout for endpoints known to be slow (items / hierarchies / sales / search ~60–90 s; lookups ~10 s). You can still override per call via the `options` argument (§6).

## Method reference

All methods are `async` and return `Promise<{ data, metadata }>` matching the HTTP API's response envelope. Methods that take a path parameter URL-encode it for you.

### System

```typescript
client.getHealth(options?)
client.getDocs(options?)
```

### Items

```typescript
client.getItems({ groups?: number[] }, options?)
client.getItemGroups(options?)
client.getItemHierarchies(options?)
client.getItemTrees(options?)
client.getItemTree(itemCode: string, options?)          // groups 131/144 only
client.getQtyPerTag(options?)
```

### Production plans

```typescript
client.getPlans(options?)
client.getPlan(planId: number | string, options?)
client.getPlanProducts(planId, options?)                // throws NotFoundError if plan missing
client.getPlanWorkOrders(planId, options?)              // same
client.getPlanTags(planId, options?)                    // same
client.getPlanSectorsSummary(planId, options?)          // same
client.getAllPlansSectorsSummary(options?)
```

### Sales / sectors

```typescript
client.getSales({ from: string, to: string }, options?) // YYYY-MM-DD
client.getProductionSectors(options?)
```

### Users / DB

```typescript
client.getUser(username: string, options?)
client.getDatabaseCompanies(options?)
```

### Tags / work orders

```typescript
client.getTag(tagEntry: number | string, options?)
client.getWorkOrderTags(workOrderEntry: number | string, options?)
```

### Machines / molds

```typescript
client.getInjectionMachines(options?)
client.getInjectionMachine(machineCode: string, options?)
client.getMolds(options?)
client.getMold(moldCode: string, options?)
```

### Business partners & contacts

```typescript
client.getBusinessPartners(options?)
client.searchBusinessPartners(criteria: SearchCriteria, options?)
client.getBusinessPartnerContacts(cardCode: string, options?)
client.getContacts(options?)
client.searchContacts(criteria: SearchCriteria, options?)
```

See [§5](#search-business-partners--contacts) for `SearchCriteria` details.

### Convenience methods

```typescript
client.planExists(planId): Promise<boolean>
client.getPlanProductsSafe(planId): { planExists: boolean; products; metadata }
client.getPlanWorkOrdersSafe(planId): { planExists: boolean; workOrders; metadata }
client.testConnection(): Promise<boolean>
```

The `*Safe` variants distinguish "plan does not exist" from "plan has no items" without throwing.

## Search (business partners & contacts)

`searchBusinessPartners` and `searchContacts` share a `SearchCriteria` shape:

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

interface SearchCriteria {
  phone?:     string;   // digits-only substring (non-digits stripped)
  email?:     string;   // exact match after canonical extraction (lowercased)
  q?:         string;   // fuzzy text via HANA CONTAINS FUZZY
  cardCode?:  string;   // exact match on CardCode
  fuzziness?: number;   // 0.1–1.0, default 0.7 — affects only q
}
```

**At least one of `phone`, `email`, `q`, or `cardCode` is required** — otherwise the server returns 400 and the client throws `ValidationError`. `fuzziness` alone is not enough.

**Scope of each criterion:**

| Criterion | `searchBusinessPartners` | `searchContacts` |
|---|---|---|
| `phone` | BP `Phone2‖Phone1` and `Phone2‖Cellular` (concatenated then digit-normalized) | contact Tel1/Tel2/Cellolar **and** parent BP phones |
| `email` | BP `E_Mail` (canonical) | contact `E_MailL` **and** parent BP email |
| `q` | CardName, City, State, Country, Block, Address, ZipCode, IndName | contact name fields **and** all BP `q` fields (cross-table) |
| `cardCode` | `c.CardCode = ?` | `cp.CardCode = ?` (scopes contacts to one BP) |

**`fuzziness` cheat-sheet:**

| Value | Behavior |
|---|---|
| `0.5` and below | Very loose; many false positives. Useful while data is heavily mistyped. |
| `0.6 – 0.7` | Loose. Catches single-letter typos and accent variants. **Default** while data is messy. |
| `0.8` | Stricter. Tolerates an accent or capitalization difference; rejects multi-character typos. |
| `0.9 – 1.0` | Near-exact. Use once data is clean. |

```typescript
// Search BPs whose name fuzzy-matches "jose silva", broad
const broad = await client.searchBusinessPartners({ q: 'jose silva', fuzziness: 0.6 });

// Search BPs by phone (formatting stripped server-side)
const byPhone = await client.searchBusinessPartners({ phone: '(11) 9-8765-4321' });

// Search BPs by exact CardCode (the "lookup" shortcut)
const oneByCode = await client.searchBusinessPartners({ cardCode: 'C12345' });

// Find contacts named "maria" within partner C12345
const scoped = await client.searchContacts({ q: 'maria', cardCode: 'C12345' });

// Cross-table: search contacts by their parent BP's city
const inSP = await client.searchContacts({ q: 'sao paulo' });
```

**Note on `getBusinessPartnerContacts(cardCode)`**: this endpoint returns `200` with an empty array even when the cardCode does not exist (it does **not** throw `NotFoundError`). If you need to verify the partner first, call `searchBusinessPartners({ cardCode })` and check the result.

## Request options & cancellation

Every method takes an optional second argument:

```typescript
interface RequestOptions {
  timeout?: number;      // override default timeout (ms)
  retries?: number;      // override retry count for this call
  signal?: AbortSignal;  // cancel an in-flight request
}

// Custom timeout for a known-slow call:
const items = await client.getItems({}, { timeout: 90000 });

// Cancellation:
const controller = new AbortController();
const promise = client.getItemHierarchies({ signal: controller.signal });
setTimeout(() => controller.abort(), 10000);

try {
  const result = await promise;
} catch (err) {
  if (err instanceof Error && err.name === 'AbortError') {
    console.log('Cancelled');
  }
}
```

## Error handling

Errors throw typed classes. All extend `HanaQueryClientError`.

```typescript
import {
  HanaQueryClientError,
  NetworkError, TimeoutError,
  ValidationError, AuthorizationError, NotFoundError, ServerError, UnknownError
} from '@ritas-inc/hanaqueryapi-client';
```

| Class | HTTP | Retryable? | When |
|---|---|---|---|
| `NetworkError` | — | yes | DNS failure, connection refused, reset |
| `TimeoutError` | — | yes | exceeded `timeout` |
| `ValidationError` | 400 | no | bad/missing parameters |
| `AuthorizationError` | 401 / 403 | no | auth failed (reserved — currently unused) |
| `NotFoundError` | 404 | no | resource missing |
| `ServerError` | 5xx | yes (limited) | HANA / server error |
| `UnknownError` | — | no | unexpected |

Every error carries a `.context` with the request URL, attempt number, duration, and the original problem details from the API:

```typescript
if (err instanceof HanaQueryClientError) {
  console.log(err.statusCode, err.message, err.context?.duration, err.context?.attempt);
}
```

### Type-guard helpers

```typescript
import {
  isNetworkError, isTimeoutError, isValidationError,
  isAuthorizationError, isNotFoundError, isServerError,
  isHanaQueryClientError, isRetryableError, getRetryDelay
} from '@ritas-inc/hanaqueryapi-client';

try {
  const result = await client.getPlan(999);
} catch (err) {
  if (isNotFoundError(err)) {
    // expected for unknown plan IDs
  } else if (isNetworkError(err) || isTimeoutError(err)) {
    // transient — retry
  } else {
    throw err;
  }
}
```

`isRetryableError(err)` returns whether the error should be retried at all; `getRetryDelay(attempt, base, max)` gives the suggested backoff.

## Request builder (fluent API)

For advanced or one-off calls you can bypass the typed methods:

```typescript
// Arbitrary endpoint with custom query params, timeout, and retries
const items = await client
  .request('/items')
  .query({ groups: [131, 144] })
  .timeout(60000)
  .retries(5)
  .execute();

// With cancellation
const controller = new AbortController();
const promise = client
  .request('/business-partners/search')
  .query({ q: 'jose', fuzziness: 0.8 })
  .signal(controller.signal)
  .execute();

setTimeout(() => controller.abort(), 5000);
```

`.execute()` returns the raw `{ success, data, metadata }` envelope (or throws on error).

## TypeScript types

Every endpoint has data, metadata, and response types exported from the package root.

### Entity types

```typescript
import type {
  ItemStatus, ItemGroup, Hierarchy, Tree, QtyPerTag,
  SalesItem, Plan, PlanProduct, WorkOrder, PlanTag, PlanSectorSummary,
  Tag, TagUsage, TagStatus,
  InjectionMachine, Mold,
  BusinessPartner, Contact
} from '@ritas-inc/hanaqueryapi-client';
```

### Data/metadata wrappers per endpoint

```typescript
import type {
  HealthData, HealthMetadata,
  DocsData, DocsMetadata,
  ItemsStatusData, ItemsStatusMetadata,
  // …one pair per endpoint…
  BusinessPartnersData, BusinessPartnersMetadata, BusinessPartnersSearchMetadata,
  ContactsData, ContactsMetadata, ContactsSearchMetadata,
  BusinessPartnerContactsMetadata
} from '@ritas-inc/hanaqueryapi-client';
```

### Response envelopes

```typescript
import type {
  HealthResponse, DocsResponse, PlansResponse, SalesResponse,
  BusinessPartnersResponse, BusinessPartnersSearchResponse,
  ContactsResponse, ContactsSearchResponse, BusinessPartnerContactsResponse,
  // …etc.
  SuccessResponse, ErrorResponse, APIResponse, ProblemDetails
} from '@ritas-inc/hanaqueryapi-client';
```

### Input types

```typescript
import type {
  SalesParams,       // { from: string; to: string }
  SearchCriteria,    // BP & contact search input
  RequestOptions,    // per-call overrides
  ClientConfig       // constructor config
} from '@ritas-inc/hanaqueryapi-client';
```

### Type guards on the response envelope

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

const raw = await client.request('/plans').execute();
if (isSuccessResponse(raw)) {
  // raw.data is typed as success
} else {
  // raw.problem is typed as error
}
```

## Recipes

### Robust retry wrapper

```typescript
import { isRetryableError, getRetryDelay, HanaQueryClientError } from '@ritas-inc/hanaqueryapi-client';

async function withRetry<T>(op: () => Promise<T>, maxAttempts = 3): Promise<T> {
  let last: HanaQueryClientError | undefined;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await op();
    } catch (err) {
      if (!(err instanceof HanaQueryClientError)) throw err;
      last = err;
      if (!isRetryableError(err) || attempt === maxAttempts) throw err;
      await new Promise(r => setTimeout(r, getRetryDelay(attempt, 1000, 10000)));
    }
  }
  throw last;
}

const plans = await withRetry(() => client.getPlans());
```

### Find a partner, then its contacts, with a graceful fallback

```typescript
const matches = await client.searchBusinessPartners({ q: 'acme widgets', fuzziness: 0.7 });
if (matches.metadata.count === 0) {
  console.log('no match');
} else {
  for (const p of matches.data.partners) {
    const { data } = await client.getBusinessPartnerContacts(p.cardcode);
    console.log(p.cardname, '->', data.contacts.length, 'contacts');
  }
}
```

### Parallel calls

```typescript
const [health, plans, items] = await Promise.all([
  client.getHealth(),
  client.getPlans(),
  client.getItems()
]);
```

### Environment-based wiring

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

const client = createClient({
  baseUrl: process.env.API_BASE_URL ?? 'http://localhost:3001'
}, {
  enableLogging: process.env.NODE_ENV !== 'production',
  logLevel: process.env.NODE_ENV === 'production' ? 'error' : 'info',
  timeout: 30000
});
```

The client itself does **not** read environment variables — your app passes them in. This keeps the package side-effect-free.

### Find contacts across a BP's name and the contact's name simultaneously

```typescript
// "jose" might be the contact's first name OR the BP's CardName
const { data } = await client.searchContacts({ q: 'jose', fuzziness: 0.7 });
data.contacts.forEach(c => console.log(c.cardname, '/', c.contactname));
```

## Development

```bash
npm install
npm run typecheck
npm run lint
npm test            # node --test on *.test.ts
npm run build       # compile to dist/
npm run example:basic    # examples/basic-usage.ts
npm run example:advanced # examples/advanced-usage.ts
npm run example:errors   # examples/error-handling.ts
```

The package is published on push to `master` via the repo's CI workflow.

## Links

- HTTP API contract: [`HANAQUERYAPI_CLIENT_MANUAL.md`](../../HANAQUERYAPI_CLIENT_MANUAL.md) at repo root
- Source repo: https://github.com/ritas-inc/hanaqueryapi
- Issues: https://github.com/ritas-inc/hanaqueryapi/issues
