npm.io
1.0.80 • Published 6d ago

@ritas-inc/hanaqueryapi-client

Licence
Version
1.0.80
Deps
0
Size
352 kB
Vulns
0
Weekly
89

@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 at the repo root.

Table of contents

  1. Install
  2. Quick start
  3. Configuration
  4. Method reference
  5. Search (business partners & contacts)
  6. Request options & cancellation
  7. Error handling
  8. Request builder (fluent API)
  9. TypeScript types
  10. Recipes

Install

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.

// 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.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.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.ts

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

Keywords