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

bms-session-client

v1.1.1

Published

Client library for the BMS Session API v2.0 — query HOSxP hospital databases (SQL + REST CRUD + server functions)

Readme

bms-session-client

Client library for the BMS Session API v2.0 — secure, typed access to HOSxP hospital databases (Bangkok Medical Software) from Node.js and TypeScript.

One BMSClient class gives you read-only SQL, config-driven REST CRUD across 118+ tables, and ~70 server-side functions, behind a validated session — with a typed error hierarchy, an optional request queue, an optional local-gateway fast path, and zero runtime dependencies.

  • Runtime: Node.js ≥ 18 (uses built-in fetch / AbortSignal.timeout)
  • Formats: ESM + CommonJS, with bundled .d.ts types
  • Dependencies: none

Contents


Install

npm install bms-session-client
// ESM / TypeScript
import { BMSClient } from "bms-session-client";
// CommonJS
const { BMSClient } = require("bms-session-client");

How it works

The BMS Session system brokers access to an on-premise HOSxP hospital database without exposing database credentials to the client. The flow has three stages:

  1. Session acquisition — a short-lived bms-session-id (a GUID) is issued by the HOSxP system, typically delivered to your app via a URL parameter, cookie, or manual entry.
  2. Session validation — validateSession(id) exchanges that GUID at https://hosxp.net/phapi/PasteJSON for connection details: the hospital's API base URL (usually a tunnel like http://tunnel.hosxp.net:PORT), a bearer token, the database name and dialect, and user/hospital metadata. The client caches these in memory for the session's lifetime (~10 hours).
  3. API execution — every subsequent call (/api/sql, /api/rest/*, /api/function) is sent to the cached base URL with the bearer token attached.

The client exposes two tiers over this flow:

| Tier | Methods | Behavior on a BMS error | |---|---|---| | Transport | validateSession, executeQuery, restRequest, callFunction | Returns the raw response; you inspect MessageCode. | | Helper | listTables, describeTable, restGet, restWrite, the function wrappers | Throws BMSApiError when MessageCode is not 2xx. |

Use the transport tier when you want full control over the response; use the helper tier for concise try/catch code. Both are on the same object — mix freely.


Quickstart

import { BMSClient } from "bms-session-client";

const client = new BMSClient({
  // optional — only needed to authorize REST/SQL writes
  marketplaceToken: process.env.BMS_MARKETPLACE_TOKEN,
});

// 1. Validate a bms-session-id → caches base URL + bearer token
const session = await client.validateSession(process.env.BMS_SESSION_ID!);
console.log(`Connected to ${session.databaseName} (${session.databaseType})`);
console.log(`User: ${session.userName} · expires in ${(session.expiresInSeconds / 3600).toFixed(1)}h`);

// 2. Read with SQL (SELECT / DESCRIBE / EXPLAIN / SHOW / WITH only)
const result = await client.executeQuery(
  "SELECT hn, fname, lname FROM patient LIMIT 5"
);
if (result.MessageCode === 200) console.table(result.data);

// 3. Read with the config-driven REST API (auto-joins + filters)
const visits = await client.restGet({
  table: "ovst",
  select: "vn,vstdate,patient(fname,lname)",
  filter: ["vstdate=gte.2026-07-01"],
  order: "vstdate.desc",
  limit: 20,
});

// 4. Discover the schema
const tables = await client.listTables("patient*");
const columns = await client.describeTable("patient");

// 5. Call a server function (via a typed wrapper)
const vn = await client.getSerialNumber("vn", "ovst", "vn");

Configuration

Every option is optional. Pass an object to the constructor:

const client = new BMSClient({
  validationUrl: "https://hosxp.net/phapi/PasteJSON", // session-exchange endpoint
  timeoutMs: 30_000,                                  // per-request timeout
  appName: "BMS.Session.Client",                      // sent as `app` on /api/sql
  marketplaceToken: undefined,                        // authorizes writes
  localApiUrl: "http://127.0.0.1:45011",              // probed by probeLocalApi()
  localProbeTimeoutMs: 3_000,                          // fast-fail for the probe
  cacheBust: false,                                    // add &random= to every URL
  queue: false,                                        // dedup + concurrency + retry
});

| Option | Type | Default | Purpose | |---|---|---|---| | validationUrl | string | https://hosxp.net/phapi/PasteJSON | Where validateSession exchanges the session ID. Override for staging/dev. | | timeoutMs | number | 30000 | Per-request timeout (via AbortSignal.timeout). Applies to every call except the local probe. | | appName | string | BMS.Session.Client | Value sent as the app field on /api/sql. Identifies your app in HOSxP audit logs. | | marketplaceToken | string | — | Authorizes REST/SQL writes. Also settable later via setMarketplaceToken(). Blank strings are ignored. | | localApiUrl | string | http://127.0.0.1:45011 | Base URL of the HOSxP local API gateway, probed by probeLocalApi(). | | localProbeTimeoutMs | number | 3000 | Timeout for the local probe — deliberately short so it never blocks connection. | | cacheBust | boolean | false | Append a unique &random=… to every request URL so no proxy/tunnel/cache returns a stale response. Recommended for tunnelled/SPA usage. | | queue | boolean \| BMSQueueOptions | false | Route executeQuery through an in-client queue (dedup + concurrency cap + 429 retry). true uses defaults; pass an object to tune. |


API reference

Session

new BMSClient(options?: BMSClientOptions)

Constructs a client. Performs no I/O — nothing is contacted until validateSession.

validateSession(sessionId: string): Promise<BMSSessionData>

Exchanges a bms-session-id for connection config and caches it in memory. Must be called before any query/REST/function method. Returns the parsed BMSSessionData.

  • Throws BMSSessionError if the exchange fails (MessageCode ≠ 200) or no auth token is present.
  • Throws BMSNetworkError on a network failure, timeout, or non-JSON body.

getSession(): BMSSessionData

Returns the cached session synchronously. Throws BMSSessionError if called before validateSession.

setMarketplaceToken(token: string | null): void

Sets or clears the write-authorization token at runtime. Blank/whitespace strings are treated as null.

hasMarketplaceToken(): boolean

Whether a non-empty marketplace token is currently configured.


SQL queries

executeQuery(sql: string, params?: SqlParams): Promise<BMSQueryResponse>

Executes a read-only statement against /api/sql. The server permits SELECT, DESCRIBE, EXPLAIN, SHOW, and WITH (CTE) — writes are rejected server-side.

  • Pass params to bind :name placeholders safely on the server (see SQL parameters).
  • Returns the raw BMSQueryResponse — inspect MessageCode yourself (transport tier).
  • Throws BMSAuthError on HTTP 501 (expired session), BMSRateLimitError on HTTP 429, BMSNetworkError on network/parse failure.
// Plain
const r = await client.executeQuery("SELECT COUNT(*) AS n FROM patient");

// With server-side parameter binding
const r2 = await client.executeQuery(
  "SELECT * FROM patient WHERE hn = :hn",
  { hn: { value: "000000001", value_type: "string" } }
);
SQL parameters (server-side binding)

SqlParams is a map of placeholder name (without the leading colon) to { value, value_type }:

await client.executeQuery(
  "SELECT * FROM ovst WHERE vstdate = :d AND hn = :hn",
  {
    d:  { value: "2026-07-01", value_type: "date" },
    hn: { value: "000000001", value_type: "string" },
  }
);

value_type ∈ string | integer | float | date | time | datetime | text.


Schema discovery

listTables(pattern?: string): Promise<string[]>

Returns table names, dialect-aware (SHOW TABLES on MariaDB, information_schema.tables on PostgreSQL). * in the pattern is translated to the SQL LIKE wildcard %.

  • The pattern is validated against /^[a-zA-Z0-9_*%]*$/; anything else throws TypeError (no network call). This is an injection guard — the pattern is interpolated into SQL.
  • Throws BMSApiError on a non-2xx response.
await client.listTables();          // all tables
await client.listTables("patient*"); // → ["patient", "patient_drug", ...]
await client.listTables("*drug*");

describeTable(table: string): Promise<Record<string, unknown>[]>

Returns column metadata for one table, dialect-aware (DESCRIBE vs information_schema.columns). Table name must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ or a TypeError is thrown. Throws BMSApiError on a non-2xx response.


REST CRUD

The /api/rest endpoint is a config-driven CRUD layer over whitelisted tables. It auto-joins related tables, applies per-table row limits, and supports rich filtering — often more convenient than hand-written SQL.

restGet(options: RestGetOptions): Promise<BMSRestResponse>

Reads records. Builds the path and query string from RestGetOptions, then throws BMSApiError on a non-2xx response.

// List with filter, ordering, pagination, and an auto-join
await client.restGet({
  table: "ovst",
  select: "vn,vstdate,patient(fname,lname),doctor(name)",
  filter: ["vstdate=gte.2026-06-01", "pttype=in.(01,02)"],
  order: "vstdate.desc",
  limit: 50,
  offset: 0,
});

// Single record by its REST id (hn for patient, vn for ovst, an for ipt)
await client.restGet({ table: "patient", id: "000000001" });
Filter operators

Each filter entry is a string "field=op.value":

| Operator | Meaning | Example | |---|---|---| | eq | equals | pttype=eq.01 | | neq | not equals | pttype=neq.01 | | gt / gte | greater / greater-or-equal | vstdate=gte.2026-06-01 | | lt / lte | less / less-or-equal | age=lt.18 | | like | SQL LIKE | fname=like.%John% | | in | in a set | pttype=in.(01,02,03) | | is | IS (null checks) | discharge=is.null |

select expansions

select takes a comma list of fields, and related(fields) expressions to pull joined data — e.g. "*,patient(fname,lname)" or "expand(all)".

restWrite(method, table, options?): Promise<BMSRestResponse>

Creates, updates, or deletes a record. Requires a marketplace token — throws TypeError without one.

  • method ∈ "POST" | "PUT" | "DELETE". PUT/DELETE require options.id (else TypeError).
  • The server validates required fields, ranges, foreign keys, and finance locks, strips protected/PII fields, and fills identity/audit fields (PKs, timestamps) itself — send only business fields.
  • Throws BMSApiError on a non-2xx response.
client.setMarketplaceToken(process.env.BMS_MARKETPLACE_TOKEN!);

await client.restWrite("POST", "oapp", { data: { hn: "000000001", nextdate: "2026-08-01" } });
await client.restWrite("PUT", "oapp", { id: "123", data: { nextdate: "2026-09-01" } });
await client.restWrite("DELETE", "oapp", { id: "123" });

restRequest(method, path, options?): Promise<BMSRestResponse>

Low-level escape hatch for /api/rest/* when restGet/restWrite don't fit. Transport tier — returns the raw response; you inspect MessageCode. The marketplace token is auto-appended as a query param on non-GET methods.

await client.restRequest("GET", "patient/000000001", { query: "select=hn,fname" });

Server functions

callFunction(name: string, payload?: Record<string, unknown>): Promise<BMSFunctionResponse>

Invokes a HOSxP server-side function via /api/function?name=… — operations that aren't plain SQL (validation, PK generation, pricing, lookups, system variables). Transport tier — returns the raw BMSFunctionResponse; most functions put their result in Value.

const r = await client.callFunction("get_serialnumber", {
  serial_name: "vn", table_name: "ovst", field_name: "vn",
});
console.log(r.Value);

Server-function wrappers

Typed convenience shells over callFunction. Each validates the response and throws BMSApiError on a non-2xx body, returning a clean value:

| Method | Returns | Function | |---|---|---| | getSerialNumber(serial, table, field) | number | get_serialnumber — generate a unique integer PK (HOSxP tables have no AUTO_INCREMENT; call immediately before INSERT). | | getNewGuid() | string | get_newguid | | getNewHn() | string | get_new_hn | | getHosVariable(name) | string | get_hosvariable — read a sys_var value (missing → ""). | | setHosVariable(name, value) | void | set_hosvariable — create/update a sys_var (needs operator rights). | | getServerDateTime() | { serverDateTime, serverDate } | get_server_datetime — server clock, for drift reconciliation. | | getHospitalInfo() | { hospitalCode, hospitalName, hospitalNameEng, hospitalAddress, hospitalTel, hospitalProvince } | get_hospital_info | | getPatientInfo(hn) | Record<string, unknown> \| null | get_patient_info | | getPatientVisitInfo(vn) | Record<string, unknown> \| null | get_patient_visit_info | | getPatientAge(hn, refDate?) | number | get_patient_age | | validateHn(hn) | boolean | validate_hn | | validateVn(vn) | boolean | validate_vn | | validateAn(an) | boolean | validate_an | | validateCid(cid) | boolean | validate_cid — Thai citizen-ID checksum. | | getIcd10Name(code) | { name, thaiName } | get_icd10_name |

const hn  = await client.getNewHn();
const vn  = await client.getSerialNumber("vn", "ovst", "vn");
const ok  = await client.validateCid("1234567890123");
const dx  = await client.getIcd10Name("J45");   // { name: "Asthma", thaiName: "โรคหืด" }

Local API gateway

When your code runs on the same machine as HOSxP, you can bypass the remote tunnel and talk to the local API server directly (127.0.0.1:45011), avoiding tunnel latency and bandwidth.

probeLocalApi(): Promise<boolean>

Sends a SELECT 1 to localApiUrl with the session's bearer token and a short timeout. If it answers MessageCode 200, all subsequent /api/* calls switch to the local base URL and true is returned. On any failure (offline, timeout, CORS) the tunnel is kept and false is returned. Requires a validated session.

isUsingLocalApi(): boolean

Whether calls are currently routed to the local gateway.

useRemoteApi(): void

Force subsequent calls back onto the remote tunnel URL.

await client.validateSession(sessionId);
if (await client.probeLocalApi()) {
  // isUsingLocalApi() === true — calls now hit 127.0.0.1:45011
}
// later, force the tunnel again:
client.useRemoteApi();

Browser note: from an HTTPS-served SPA, calling http://127.0.0.1:45011 is subject to mixed-content and CORS rules and may be blocked unless the local server sends CORS headers. From Node it is unrestricted.


Request queue

Enable with queue: true (defaults) or queue: { … } (tuned). When enabled, executeQuery is routed through an in-client queue that:

  • De-duplicates identical concurrent queries — same SQL + params + connection share a single in-flight HTTP request.
  • Caps concurrency at maxConcurrent (default 3).
  • Retries on HTTP 429 (BMSRateLimitError) with exponential backoff + jitter, honouring the server's Retry-After when present.
const client = new BMSClient({
  queue: { maxConcurrent: 3, maxRetryAttempts: 3, baseRetryDelayMs: 1000, maxRetryDelayMs: 30000 },
});

client.getQueueStats(); // { pending, active, completed, failed } — or null if no queue
client.clearQueue();    // reject + drop pending requests (call on disconnect)

The queue wraps reads only — REST and function writes are never de-duplicated. The BMSRequestQueue class is also exported if you want to reuse the mechanism standalone.


Error handling

Every error the library throws extends Error via the abstract BMSError, so existing catch (e) / e instanceof Error code keeps working. Catch by subclass to branch:

| Class | Thrown when | Extra fields | |---|---|---| | BMSSessionError | Session validation failed, or a method was called before validateSession | — | | BMSAuthError | HTTP 501 from an /api/* endpoint — session expired/invalid | endpoint | | BMSApiError | A helper-tier call received a non-2xx MessageCode | messageCode, endpoint | | BMSRateLimitError | HTTP 429 — you are being rate-limited | retryAfterSeconds, endpoint | | BMSNetworkError | fetch rejected, timed out, or the body wasn't JSON | cause |

Programmer errors — invalid table/function name, missing id on PUT/DELETE, restWrite without a token, an out-of-charset listTables pattern — throw plain TypeError, because they're caller bugs, not API conditions.

import {
  BMSAuthError, BMSApiError, BMSRateLimitError, BMSNetworkError,
} from "bms-session-client";

try {
  const rows = await client.restGet({ table: "ovst", limit: 10 });
} catch (e) {
  if (e instanceof BMSAuthError) {
    // session expired → re-validate with a fresh session ID
    await client.validateSession(await getFreshSessionId());
  } else if (e instanceof BMSRateLimitError) {
    await sleep((e.retryAfterSeconds ?? 5) * 1000); // then retry
  } else if (e instanceof BMSApiError) {
    console.error(`BMS ${e.messageCode} on ${e.endpoint}: ${e.message}`);
  } else if (e instanceof BMSNetworkError) {
    console.error("Network/parse failure:", e.cause);
  } else {
    throw e; // TypeError or something unexpected
  }
}

Types

All types are exported for import (import type { … } from "bms-session-client").

BMSSessionData

Returned by validateSession / getSession:

| Field | Type | Notes | |---|---|---| | bmsUrl | string | API base URL (tunnel or local gateway). | | bearerToken | string | Bearer token attached to every call. | | databaseType | "MariaDB" \| "PostgreSQL" | Drives dialect in listTables/describeTable. | | databaseName | string | Connected schema/database. | | userName, position, hospitalCode, department | string | User/hospital metadata. | | version | string | HOSxP system version. | | expiresInSeconds | number | Session lifetime (typically 36000 = 10h). |

BMSQueryResponse

MessageCode, Message, RequestTime?, data?: Record<string, unknown>[], field?: number[], field_name?: string[], record_count?: number.

BMSRestResponse

As above plus limit?, offset?, insert_count?, update_count?; data may be a single object (by-id read) or an array (list).

BMSFunctionResponse

MessageCode, Message?, RequestTime?, Value? (most functions), value?, plus an index signature for function-specific fields (e.g. server_datetime, hospital_code, valid).

RestGetOptions

table (required), id?, select?, filter?: string[], limit?, offset?, order?.

SqlParam / SqlParams

SqlParam = { value: string | number; value_type: SqlParamValueType }. SqlParams = Record<string, SqlParam>.

BMSQueueOptions / BMSQueueStats

Options: maxConcurrent?, maxRetryAttempts?, baseRetryDelayMs?, maxRetryDelayMs?. Stats: pending, active, completed, failed.

Other exported names

BMSDatabaseType, SqlParamValueType, BMSClientOptions, BMSSessionResponse, and the classes BMSClient, BMSRequestQueue, plus the five error classes.


HOSxP domain notes

  • Identifiers: HN = hospital number (patient), VN = visit number (OPD), AN = admission number (IPD). Many tables also carry a hos_guid. These are the usual REST id values.
  • Common tables: patient (patients), ovst / opd (outpatient visits), ipt (inpatient admissions), doctor, drugitems (medications), oapp (appointments).
  • Dates: HOSxP frequently stores Thai Buddhist-era dates (e.g. 2569-12-22 = 2026-12-22). Convert on display as needed.
  • Field type codes (for write payloads): 1=Boolean, 2=Integer, 3=Float, 4=DateTime, 5=Time, 6=UTF8Text/String, 7=Blob (Base64), 9=UTF8Text.
  • PII masking: the server pre-masks sensitive fields (CID, names, phones, addresses, emails) — masked values like 1234****890 or ส*** are normal, not corruption. Display them as-is.
  • Restricted tables: opduser, opdconfig, sys_var, user_var, user_jwt are not directly queryable.
  • Query limits: /api/sql is read-only and allows at most 20 tables per query; use single quotes for string literals.

Recipes

Node script (read-only)

import { BMSClient } from "bms-session-client";

const client = new BMSClient({ cacheBust: true });
await client.validateSession(process.env.BMS_SESSION_ID!);

const today = await client.executeQuery(
  "SELECT COUNT(*) AS n FROM ovst WHERE vstdate = CURDATE()"
);
console.log("OPD visits today:", today.data?.[0]?.n);

Same-host deployment (prefer the local gateway)

const client = new BMSClient({ cacheBust: true });
await client.validateSession(sessionId);
await client.probeLocalApi(); // silently upgrades to 127.0.0.1:45011 if present

High-throughput dashboard (queue on)

const client = new BMSClient({ queue: { maxConcurrent: 3 }, cacheBust: true });
await client.validateSession(sessionId);

// Fired in parallel from many widgets — identical queries de-dupe,
// concurrency is capped, and 429s retry automatically.
const [kpi, trend, depts] = await Promise.all([
  client.executeQuery(KPI_SQL),
  client.executeQuery(TREND_SQL),
  client.executeQuery(DEPT_SQL),
]);

Insert with a generated primary key

client.setMarketplaceToken(process.env.BMS_MARKETPLACE_TOKEN!);

const orderId = await client.getSerialNumber("refill_order_id", "refill_order", "order_id");
await client.restWrite("POST", "refill_order", {
  data: { order_id: orderId, hn: "000000001" },
});

Troubleshooting

| Symptom | Likely cause | Fix | |---|---|---| | BMSSessionError: Session validation failed | Expired or wrong bms-session-id | Obtain a fresh session ID from HOSxP and re-validate. | | BMSAuthError mid-session | Session expired (10h lifetime) or token no longer valid | Re-run validateSession with a new ID. | | BMSApiError (500): Invalid Key data … | A server function is missing a required payload key | Supply the named key (the message names it). | | TypeError: Write operations require a marketplace token | restWrite without a token | setMarketplaceToken(...) or pass marketplaceToken to the constructor. | | TypeError: Invalid table pattern | listTables pattern has disallowed characters | Use only letters, digits, _, *, %. | | Stale data through a tunnel/proxy | Cached responses | Construct with cacheBust: true. | | BMSRateLimitError | HTTP 429 | Back off retryAfterSeconds, or enable queue to retry automatically. |


Versioning

Semantic versioning. 1.1.0 added the local-API gateway probe, cache-busting, BMSRateLimitError (429 handling), SQL named parameters, the opt-in request queue, detectDatabaseType(), and the server-function wrappers — all additive over 1.0.0.

License

MIT