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

@eka-care/medical-records-ts-sdk

v1.1.9

Published

TypeScript SDK for Eka Care Medical Records API

Readme

@eka-care/medical-records-ts-sdk

TypeScript SDK for the Eka Care Medical Records API. Offline-first, works in both browser and Electron renderer.


Table of Contents

  1. Installation
  2. Quick Start
  3. Configuration
  4. Authentication
  5. Records
  6. Cases
  7. Reactive UI Updates
  8. Logging
  9. Offline Support
  10. Electron Setup
  11. Error Handling
  12. Document Types
  13. Running the Demo

1. Installation

npm install @eka-care/medical-records-ts-sdk

2. Quick Start

import { MedicalRecordsClient } from '@eka-care/medical-records-ts-sdk';

const sdk = new MedicalRecordsClient({
  baseUrl: '',
  defaultHeaders: {
    'client-id': 'your-client-id',
    'flavour':   'your-flavour',
    'b-tid':     'your-business-id',   // business tenant ID — sent on every API request
  },
  cache: { oid: 'logged-in-user-oid' },
  onError: (err) => console.error(err),
});

sdk.setAuthToken('eyJhbGciOi...');

const docs = await sdk.listDocuments({ bid: 'your-business-id', patientId: 'oid' });

3. Configuration

new MedicalRecordsClient(config: SDKConfig)

| Field | Type | Required | Description | |---|---|---|---| | baseUrl | string | No | Override base URL. Pass '' for same-origin. Defaults to https://api.eka.care | | environment | 'prod' \| 'dev' | No | prod = api.eka.care, dev = api.dev.eka.care. Ignored if baseUrl is set | | defaultHeaders | Record<string, string> | Yes | Required Eka headers — see below | | cache | CacheConfig | No | Enables IndexedDB offline cache | | cache.oid | string | Yes (if cache) | Logged-in user's OID — default patient scope | | transport | 'native' \| 'bridge' | No | native = browser fetch (default). bridge = Electron IPC | | onError | (err: unknown) => void | No | Called on every SDK error | | onUnauthorized | () => Promise<string \| undefined> | No | Called on 401 — return a fresh token to auto-retry | | logLevel | 'debug' \| 'info' \| 'warn' \| 'error' \| 'none' | No | Minimum log level. Default: 'none' | | onLog | (log: SDKLog) => void | No | Receives structured log entries at or above logLevel |

Required headers

defaultHeaders: {
  'client-id': 'doc-web',       // app identifier
  'flavour':   'ekascribe-web', // app variant
  'b-tid':     'BID',           // business tenant ID — sent on every API request
}

b-tid (business tenant ID) is required by the Eka API edge for routing and access control. It is merged into every HTTP request automatically.

Note: bid (business ID) is also passed per SDK call — it scopes IndexedDB queries client-side. Both carry the same value.

Full config example

const sdk = new MedicalRecordsClient({
  baseUrl: 'https://api.eka.care',
  defaultHeaders: {
    'client-id': 'doc-web',
    'flavour':   'ekascribe-web',
    'b-tid':     'BID',
  },
  cache: { oid: 'USER_OID' },
  transport: 'native',
  logLevel: 'info',
  onLog: ({ level, message, context }) => console.log(`[${level}] ${message}`, context),
  onError: (err) => console.error('[SDK]', err),
  onUnauthorized: async () => await refreshToken(),
});

4. Authentication

sdk.setAuthToken('eyJhbGciOi...');  // set after login
sdk.setAuthToken(undefined);         // clear (falls back to cookie auth)

Sent as Authorization: Bearer <token> on every request. On 401, onUnauthorized is called — return a fresh token to auto-retry once.


5. Records

Every records method requires bid and patientId:

const BID = 'your-business-id';
const OID = 'logged-in-user-oid';

Linked profiles (primary + linked OIDs)

patientId is the primary OID — the profile that owns writes (add/edit/delete use it for the X-Pt-Id header). To also surface records/cases from linked profiles (e.g. dependents), pass linkedPatientIds. Read and list/group methods then return the union across [patientId, ...linkedPatientIds], and the background sync fans out one fetch per OID (each with its own X-Pt-Id). Omit linkedPatientIds for single-profile behavior — nothing changes.

const docs = await sdk.listDocuments({
  bid: BID,
  patientId: OID,                       // primary — owns writes
  linkedPatientIds: ['dependent-oid-1', 'dependent-oid-2'], // also read these
});

Supported on: listDocuments, listLocalDocuments, groupDocuments, getAllTags, listCases, listLocalCases. Writes (addDocument, editDocument, deleteDocument, createCase, …) always use the primary patientId.


List Documents

Delta sync — fetches only records newer than the local watermark.

// All documents
const docs = await sdk.listDocuments({ bid: BID, patientId: OID });

// With limit and sort
const docs = await sdk.listDocuments({
  bid: BID, patientId: OID,
  limit: 20,                        // default: no limit
  sortBy: 'createdAt',              // 'updatedAt' (default) | 'createdAt'
});

// Filter by record type
const labReports = await sdk.listDocuments({
  bid: BID, patientId: OID,
  filter: { recordType: 'lr' },     // lr | ps | dc | vc | in | iv | sc | op
});

// Filter by case
const caseRecords = await sdk.listDocuments({
  bid: BID, patientId: OID,
  filter: { caseId: 'case-uuid' },
});

// Stale-while-revalidate — show cached instantly, update when fresh arrives
const docs = await sdk.listDocuments({
  bid: BID, patientId: OID,
  onStale: (cached) => setDocs(cached),
});
setDocs(docs);

// Local DB only — no network call
const docs = await sdk.listLocalDocuments({ bid: BID, patientId: OID });

ListDocumentsFilter fields:

| Field | Type | Description | |---|---|---| | recordType | DocumentType | Filter by document type ('lr', 'ps' etc.) — client-side | | caseId | string | Filter to records belonging to a case — client-side | | fileType | string | Filter by file type ('IMG', 'PDF', 'HTML') — client-side | | u_at__gt | number | Only records updated after this epoch — sent to server |


Group Documents

Groups records from local DB — no network call.

// Group by document type
const groups = await sdk.groupDocuments({
  bid: BID, patientId: OID,
  groupBy: 'recordType',
});
// → [{ key: 'lr', label: 'Lab Report', records: [...] }, ...]

// Group by month (default)
const groups = await sdk.groupDocuments({
  bid: BID, patientId: OID,
  groupBy: 'date',
  dateFormat: 'month',       // 'day' | 'month' (default) | 'year'
  dateField: 'documentDate', // 'documentDate' (default) | 'createdAt'
});
// → [{ key: '2025-05', label: 'May 2025', records: [...] }, ...]

GroupDocumentsOptions:

| Field | Type | Default | Description | |---|---|---|---| | groupBy | 'recordType' \| 'date' | required | How to group | | dateFormat | 'day' \| 'month' \| 'year' | 'month' | Granularity when groupBy: 'date' | | dateField | 'documentDate' \| 'createdAt' | 'documentDate' | Which date to group by | | sortBy | 'updatedAt' \| 'createdAt' | 'updatedAt' | Sort within each group | | filter | ListDocumentsFilter | — | Same filters as listDocuments |


Download a Document

Cache-first — returns blobs from IndexedDB if previously fetched, otherwise downloads from S3 and caches.

const files = await sdk.downloadFile({ bid: BID, patientId: OID, documentId: 'abc-uuid' });

for (const file of files) {
  console.log(file.blob);       // Blob — PDF / image / HTML
  console.log(file.fileType);   // 'IMG' | 'PDF' | 'HTML'
  console.log(file.filename);   // e.g. 'abc12345_0.pdf'
  console.log(file.fromCache);  // true = served from IndexedDB, zero network
}

// Trigger browser download
const a = document.createElement('a');
a.href = URL.createObjectURL(files[0].blob);
a.download = files[0].filename;
a.click();

Get Smart Report

Cache-first — returns the AI-extracted smart report from IndexedDB if available, otherwise fetches from API.

const report = await sdk.getSmartReport({
  bid: BID, patientId: OID,
  documentId: 'abc-uuid',
});

if (report) {
  console.log(report.verified);    // AI-verified fields
  console.log(report.unverified);  // AI-unverified fields
}
// null = document has no smart report

Upload a Document

const file = input.files[0];

await sdk.addRecord({
  bid: BID, patientId: OID,
  batchRequests: [{
    dt: 'lr',
    dd_e: Math.floor(Date.now() / 1000),
    cases: ['case-uuid'],                        // optional — assign to case
    files: [{ contentType: 'application/pdf', file_size: file.size }],
  }],
  files: [[file]],
  filenames: [[file.name]],
});

Offline: record + blob saved to IndexedDB as upload_failure. Auto-retried on next sdk.sync().

File constraints:

| | Limit | |---|---| | Max batch items | 5 | | Max files per batch | 10 | | Image max | 10 MB | | PDF max | 25 MB | | Supported types | image/jpeg image/png application/pdf |


Describe a Document

Returns file URLs, smart report, tags.

const detail = await sdk.describeDocument({
  bid: BID, patientId: OID,
  documentId: 'abc-uuid',
});

// detail.files[0].asset_url  — signed S3 URL
// detail.smart_report        — AI-extracted fields
// detail.tags                — user tags

Edit a Document

Local-first — patches DB immediately, syncs to server in background.

await sdk.editRecord({
  bid: BID, patientId: OID,
  documentId: 'abc-uuid',
  data: {
    dt:    'ps',                 // new document type
    dd_e:  1748000000,           // new document date (epoch)
    cases: ['case-id-1'],        // link to cases (replaces existing)
    tg:    ['blood', 'routine'], // tags (replaces existing)
  },
});

Delete a Document

Soft-deletes locally (hidden immediately), hard-deletes after server confirms. Also removes the document from any linked cases in local DB.

await sdk.deleteRecord({ bid: BID, patientId: OID, documentId: 'abc-uuid' });

6. Cases

Cases act as folders for records. A record can belong to multiple cases.

List Cases

const cases = await sdk.listCases({ bid: BID, patientId: OID });

// Local DB only:
const cases = await sdk.listLocalCases({ bid: BID, patientId: OID });

// Records inside a case:
const docs = await sdk.listLocalDocuments({
  bid: BID, patientId: OID,
  filter: { caseId: 'case-uuid' },
});

Create a Case

await sdk.createCase({
  bid: BID, patientId: OID,
  data: {
    id:           crypto.randomUUID(),
    display_name: 'Follow-up visit',
    type:         'OP',                             // EM | IP | OP | HH
    occurred_at:  Math.floor(Date.now() / 1000),    // required
  },
});

Update a Case

await sdk.updateCase({
  bid: BID, patientId: OID,
  caseId: 'case-uuid',
  data: { display_name: 'New name', type: 'IP', occurred_at: 1748000000 },
});

Delete a Case

Deletes from server and removes the case from all linked records' caseIDs in local DB.

await sdk.deleteCase({ bid: BID, patientId: OID, caseId: 'case-uuid' });

7. Reactive UI Updates

Subscribe to DB change events — the UI auto-updates on any write (upload, edit, delete, sync).

// Subscribe once (e.g. in useEffect)
const unsub = sdk.subscribe('records:changed', () => {
  sdk.listLocalDocuments({ bid, patientId }).then(setRecords);
});

const unsubCases = sdk.subscribe('cases:changed', () => {
  sdk.listLocalCases({ bid, patientId }).then(setCases);
});

// Cleanup on unmount
return () => { unsub(); unsubCases(); };

Events:

  • records:changed — fired after any record write (upload, edit, delete, sync)
  • cases:changed — fired after any case write

Pending sync count:

const { records, cases } = await sdk.getPendingCounts();
// records — count of unsynced records (uploading / edited / archived)
// cases   — count of unsynced cases (not yet created or edited on server)

8. Logging

All internal SDK logs are prefixed with EkaMR: and routed through your onLog callback.

import type { SDKLog } from '@eka-care/medical-records-ts-sdk';

const sdk = new MedicalRecordsClient({
  logLevel: 'info',   // 'debug' | 'info' | 'warn' | 'error' | 'none' (default)
  onLog: (log: SDKLog) => {
    // Forward to your logging system
    console.log(`[${log.level}] ${log.message}`, log.context);
    // or: Sentry.addBreadcrumb({ message: log.message, data: log.context })
    // or: datadogLogs.logger.info(log.message, log.context)
  },
});

Log levels:

| Level | What is logged | |---|---| | debug | Watermark values, cache hit/miss, per-record sync results | | info | Sync started/completed, N records synced, upload complete | | warn | Network failed → serving stale cache, server sync failed → will retry | | error | S3 upload failed with document IDs | | none | All logging disabled (default) |

SDKLog shape:

interface SDKLog {
  level:     'debug' | 'info' | 'warn' | 'error';
  message:   string;                        // e.g. "EkaMR: sync: started"
  context?:  Record<string, unknown>;       // structured data (count, documentId, etc.)
  timestamp: number;                        // Unix milliseconds
}

9. Offline Support

The SDK stores everything in IndexedDB and syncs in the background.

What happens offline

| Action | Offline behavior | |---|---| | listDocuments | Returns cached records immediately | | listCases | Returns cached cases immediately | | addRecord | Saves record + blob as upload_failure — retried on reconnect | | editRecord | Patches DB (isEdited: true) — synced on reconnect | | deleteRecord | Soft-deletes (isArchived: true) — hard-deleted on reconnect | | createCase | Saves locally (isRemoteCreated: false) — created on reconnect |

Sync order

Sync always runs: cases → uploads → edits + deletes (parallel).

// Auto-runs on every SDK init (page load)
// Manual trigger:
window.addEventListener('online', () => sdk.sync());
await sdk.sync();

Clear cache on logout

import { clearDb } from '@eka-care/medical-records-ts-sdk';
await clearDb();

10. Electron Setup

IndexedDB works natively in the Electron renderer process. Only the transport differs:

// Electron
new MedicalRecordsClient({ transport: 'bridge', cache: { oid }, defaultHeaders: {...} })

// Browser
new MedicalRecordsClient({ transport: 'native', cache: { oid }, defaultHeaders: {...} })

bridge routes Eka API calls through window.networkApi (IPC → main → net.fetch). S3 uploads always use fetch() directly — the IPC bridge cannot carry binary data.


11. Error Handling

import { EkaCareApiError, UploadFailedError } from '@eka-care/medical-records-ts-sdk';

try {
  await sdk.addRecord({ ... });
} catch (err) {
  if (err instanceof UploadFailedError) {
    // Auth succeeded but S3 upload failed — record saved as upload_failure
    // sdk.sync() retries automatically
    console.error('Failed IDs:', err.failures.map(f => f.documentId));
  } else if (err instanceof EkaCareApiError) {
    console.error(err.message, err.statusCode, err.responseBody);
  }
}

editRecord, deleteRecord, createCase, updateCase, deleteCase never throw on network failure — local state is always updated and server sync retries automatically.


12. Document Types

| Code | Label | |---|---| | ps | Prescription | | lr | Lab Report | | dc | Discharge Summary | | vc | Vaccine Certificate | | in | Insurance | | iv | Invoice | | sc | Scan | | op | Other |

import { DOCUMENT_TYPE_LABELS, DOCUMENT_TYPE_OPTIONS } from '@eka-care/medical-records-ts-sdk';

DOCUMENT_TYPE_OPTIONS  // [{ value: 'lr', label: 'Lab Report' }, ...]  — for dropdowns
DOCUMENT_TYPE_LABELS   // { lr: 'Lab Report', ps: 'Prescription', ... } — for display

13. Running the Demo

packages/ui is a React playground — not published.

# Install
npm install && cd packages/ui && npm install

# Credentials — edit packages/ui/src/example/sdk/client.ts
export const OID         = 'your-oid';
export const BUSINESS_ID = 'your-business-id';

# Start
cd packages/ui && npm run dev
# Open https://test.eka.care:5173

Building

npm run build       # TypeScript → dist/

Publishing

npm version patch
npm publish --access public