UNPKG

@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
# @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.11.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.60.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.91.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