@ritas-inc/hanaqueryapi-client
v1.0.78
Published
TypeScript client for HANA Query API with full type safety and error handling
Maintainers
Readme
@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.mdat the repo root.
Table of contents
- Install
- Quick start
- Configuration
- Method reference
- Search (business partners & contacts)
- Request options & cancellation
- Error handling
- Request builder (fluent API)
- TypeScript types
- Recipes
Install
npm install @ritas-inc/hanaqueryapi-clientRequirements
- Node.js ≥ 24.0.0 (uses native TypeScript /
--experimental-strip-types). - TypeScript ≥ 5 if you want compile-time types (the package ships both source
.tsand built.d.ts).
Breaking change since v1.0.0
baseUrl is required when constructing the client. The zero-arg default constructor was removed.
// 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
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).
Configuration
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 |
import { createClientFromEnvironment } from '@ritas-inc/hanaqueryapi-client';
const client = createClientFromEnvironment('development');Override individual fields:
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
client.getHealth(options?)
client.getDocs(options?)Items
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
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
client.getSales({ from: string, to: string }, options?) // YYYY-MM-DD
client.getProductionSectors(options?)Users / DB
client.getUser(username: string, options?)
client.getDatabaseCompanies(options?)Tags / work orders
client.getTag(tagEntry: number | string, options?)
client.getWorkOrderTags(workOrderEntry: number | string, options?)Machines / molds
client.getInjectionMachines(options?)
client.getInjectionMachine(machineCode: string, options?)
client.getMolds(options?)
client.getMold(moldCode: string, options?)Business partners & contacts
client.getBusinessPartners(options?)
client.searchBusinessPartners(criteria: SearchCriteria, options?)
client.getBusinessPartnerContacts(cardCode: string, options?)
client.getContacts(options?)
client.searchContacts(criteria: SearchCriteria, options?)See §5 for SearchCriteria details.
Convenience methods
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:
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. |
// 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:
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.
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:
if (err instanceof HanaQueryClientError) {
console.log(err.statusCode, err.message, err.context?.duration, err.context?.attempt);
}Type-guard helpers
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:
// 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
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
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
import type {
HealthResponse, DocsResponse, PlansResponse, SalesResponse,
BusinessPartnersResponse, BusinessPartnersSearchResponse,
ContactsResponse, ContactsSearchResponse, BusinessPartnerContactsResponse,
// …etc.
SuccessResponse, ErrorResponse, APIResponse, ProblemDetails
} from '@ritas-inc/hanaqueryapi-client';Input types
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
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
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
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
const [health, plans, items] = await Promise.all([
client.getHealth(),
client.getPlans(),
client.getItems()
]);Environment-based wiring
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
// "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
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.tsThe package is published on push to master via the repo's CI workflow.
Links
- HTTP API contract:
HANAQUERYAPI_CLIENT_MANUAL.mdat repo root - Source repo: https://github.com/ritas-inc/hanaqueryapi
- Issues: https://github.com/ritas-inc/hanaqueryapi/issues
