@ritas-inc/hanaqueryapi-client
Version:
TypeScript client for HANA Query API with full type safety and error handling
534 lines (409 loc) • 17.1 kB
Markdown
# @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