npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ritas-inc/hanaqueryapi-client

v1.0.78

Published

TypeScript client for HANA Query API with full type safety and error handling

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

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

Links

  • HTTP API contract: HANAQUERYAPI_CLIENT_MANUAL.md at repo root
  • Source repo: https://github.com/ritas-inc/hanaqueryapi
  • Issues: https://github.com/ritas-inc/hanaqueryapi/issues