@eka-care/medical-records-ts-sdk
v1.2.1
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
- Installation
- Quick Start
- Configuration
- Authentication
- Records
- Cases
- Vitals & Parameter Trends
- Reactive UI Updates
- Logging
- Offline Support
- Electron Setup
- Error Handling
- Document Types
- Running the Demo
1. Installation
npm install @eka-care/medical-records-ts-sdk2. 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 defaults to true — IndexedDB offline cache is enabled unless you pass `cache: false`
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 |
|---|---|---|---|
| defaultHeaders | Record<string, string> | Effectively yes | Required Eka headers — see below |
| baseUrl | string | No | Override the API base URL. Pass '' for same-origin. Takes precedence over environment |
| vaultBaseUrl | string | No | Override the vault base URL (used by getParameterTrend). Pass '' for same-origin |
| environment | SDKEnvironment | No | SDKEnvironment.Prod = api.eka.care, SDKEnvironment.Dev = api.dev.eka.care. Ignored if baseUrl is set. Default: Prod |
| cache | boolean | No | IndexedDB offline cache. Defaults to true. Pass false to run network-only |
| transport | TransportMode | No | TransportMode.Native = browser fetch (default). TransportMode.Bridge = Electron IPC |
| onError | (err: unknown) => void | No | Called on every unhandled SDK error (error is still re-thrown) |
| onUnauthorized | () => Promise<string \| undefined> \| string \| undefined | No | Called on 401 — return a fresh token to auto-retry once |
| onLog | (log: SDKLog) => void | No | Receives every structured log event emitted by the SDK |
Note:
cacheno longer takes anoid. Patient scope is passed per call viapatientId(see Records).logLevelhas been removed — every event flows throughonLog(see Logging).
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
import { MedicalRecordsClient, SDKEnvironment, TransportMode } from '@eka-care/medical-records-ts-sdk';
const sdk = new MedicalRecordsClient({
environment: SDKEnvironment.Prod, // or set baseUrl / vaultBaseUrl explicitly
defaultHeaders: {
'client-id': 'doc-web',
'flavour': 'ekascribe-web',
'b-tid': 'BID',
},
cache: true,
transport: TransportMode.Native,
onLog: (log) => console.log(`[${log.eventName}] ${log.status}`, log.params),
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 the failed request 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.
import { SortBy } from '@eka-care/medical-records-ts-sdk';
// 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: SortBy.CreatedAt, // SortBy.UpdatedAt (default) | SortBy.CreatedAt
});
// Filter by document type
const labReports = await sdk.listDocuments({
bid: BID, patientId: OID,
filter: { documentType: 'lr' }, // client-side filter by type code
});
// 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);
// Force a full refresh — ignore the local watermark and re-fetch everything
const docs = await sdk.listDocuments({ bid: BID, patientId: OID, forceRefresh: true });
// Local DB only — no network call
const docs = await sdk.listLocalDocuments({ bid: BID, patientId: OID });ListDocumentsFilter fields:
| Field | Type | Description |
|---|---|---|
| documentType | string | Filter by document type code ('lr', 'ps' etc.) — client-side |
| caseId | string | Filter to records belonging to a case — client-side |
| tags | string[] | Only records that have all of these tags — client-side |
| fileType | string | Filter by file type ('IMG', 'PDF', 'HTML') — client-side |
| maxFileTypeFetch | number | Max documents to collect when fileType is set. Default: 3 |
| updatedAfter | number | Only records updated after this epoch — sent to server as u_at__gt |
Each returned DocumentItem also carries is_smart / is_analyzing (AI-report state),
sync_state (local sync status when served from cache), and total_vitals /
out_of_range_vitals (parameter counts extracted from the document).
Group Documents
Groups records from local DB — no network call.
import { GroupBy, DateGroupFormat, DateField } from '@eka-care/medical-records-ts-sdk';
// Group by document type
const groups = await sdk.groupDocuments({
bid: BID, patientId: OID,
groupBy: GroupBy.DocumentType,
});
// → [{ key: 'lr', label: 'Lab Report', records: [...] }, ...]
// Group by month (default)
const groups = await sdk.groupDocuments({
bid: BID, patientId: OID,
groupBy: GroupBy.Date,
dateFormat: DateGroupFormat.Month, // Day | Month (default) | Year
dateField: DateField.DocumentDate, // DocumentDate (default) | CreatedAt
});
// → [{ key: '2025-05', label: 'May 2025', records: [...] }, ...]GroupDocumentsOptions:
| Field | Type | Default | Description |
|---|---|---|---|
| groupBy | GroupBy | required | DocumentType or Date |
| dateFormat | DateGroupFormat | Month | Granularity when groupBy: Date |
| dateField | DateField | DocumentDate | Which date to group by |
| sortBy | SortBy | 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); // DocumentFileType.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 items
console.log(report.unverified); // AI-unverified items
}
// null = document has no smart reportDescribe 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 tagsUpload a Document
addDocument runs the full two-step flow — obtains presigned S3 URLs then uploads every file.
const file = input.files[0];
await sdk.addDocument({
bid: BID, patientId: OID,
batchRequests: [{
documentType: 'lr',
documentDate: 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]],
});Need finer control? obtainAuthorization (step 1 — presigned URLs) and uploadFile
(step 2 — one file to S3) are exposed separately.
Offline: record + blob saved to IndexedDB as upload_failure. Auto-retried on next sdk.sync().
File constraints (also exported as UPLOAD_CONSTRAINTS):
| | Limit |
|---|---|
| Max batch items | 5 |
| Max files per batch | 10 |
| Image max | 10 MB |
| PDF max | 25 MB |
| Supported types | image/jpeg image/jpg image/png application/pdf |
Edit a Document
Local-first — patches DB immediately, syncs to server in background.
await sdk.editDocument({
bid: BID, patientId: OID,
documentId: 'abc-uuid',
data: {
documentType: 'ps', // new document type
documentDate: 1748000000, // new document date (epoch)
cases: ['case-id-1'], // link to cases (replaces existing)
tags: ['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.deleteDocument({ bid: BID, patientId: OID, documentId: 'abc-uuid' });Tags
// Every unique tag across all local documents — sorted alphabetically
const allTags = await sdk.getAllTags({ bid: BID, patientId: OID });
// Tags for one document (from local DB)
const tags = await sdk.getDocumentTags({ bid: BID, patientId: OID, documentId: 'abc-uuid' });Tags are written via editDocument({ data: { tags: [...] } }) (replaces the full set).
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),
},
});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' cases in local DB.
await sdk.deleteCase({ bid: BID, patientId: OID, caseId: 'case-uuid' });7. Vitals & Parameter Trends
Fetch the trend (all readings over time) for a single vital/lab parameter — e.g. Hemoglobin. Hits the vault host. Network-only — not cached.
import { VitalResultFilter } from '@eka-care/medical-records-ts-sdk';
const trend = await sdk.getParameterTrend({
bid: BID, patientId: OID,
parameterId: 'lb-2139115007', // eka parameter id
filter: VitalResultFilter.All, // All (default) | High | Low | Normal
});
console.log(trend.name); // 'Hemoglobin (Hb)'
console.log(trend.unit); // 'g/dL'
for (const log of trend.vitals_info) {
console.log(log.date, log.val, log.display_result); // newest first
console.log(log.document_id); // pass to describeDocument
}8. 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('documents: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:
documents:changed— fired after any record write (upload, edit, delete, sync)cases:changed— fired after any case write
Pending sync count:
const { documents, cases } = await sdk.getPendingCounts();
// documents — count of unsynced records (uploading / edited / archived)
// cases — count of unsynced cases (not yet created or edited on server)Last server refresh:
sdk.sourceRefreshedAt; // Unix epoch of the last background refresh, or null before the first listDocuments9. Logging
Every internal SDK operation emits a structured SDKLog event through your onLog callback —
there is no log-level filtering; you receive all events and decide what to keep.
import type { SDKLog } from '@eka-care/medical-records-ts-sdk';
const sdk = new MedicalRecordsClient({
defaultHeaders: { /* ... */ },
onLog: (log: SDKLog) => {
// Forward to your logging / analytics system
console.log(`[${log.eventName}] ${log.status}`, log.params);
// e.g. Mixpanel.track(log.eventName, { ...log.params, status: log.status })
},
});SDKLog shape:
interface SDKLog {
eventName: string; // Mixpanel-ready, e.g. "Records_TS_SDK_CREATE"
eventType: EventType; // create | read | update | delete
status: EventStatus; // success | failure
platform: EventPlatform; // database | network
entityType: EventEntityType; // records | cases
params?: Record<string, unknown>; // structured data (counts, document IDs, error details)
message?: string; // optional human-readable description
patientOid?: string; // OID the event is scoped to
bid?: string; // business / tenant ID the event is scoped to
timestamp: number; // Unix milliseconds
}EventType, EventStatus, EventPlatform, and EventEntityType are exported enums.
10. 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 |
| addDocument | Saves record + blob as upload_failure — retried on reconnect |
| editDocument | Patches DB (isEdited: true) — synced on reconnect |
| deleteDocument | Soft-deletes (isArchived: true) — hard-deleted on reconnect |
| createCase | Saves locally (isRemoteCreated: false) — created on reconnect |
Sync order
Sync runs in dependency order: case creates → record uploads → (case edits, case deletes, record edits, record deletes) in parallel.
// Auto-runs on every SDK init (page load)
// Manual trigger:
window.addEventListener('online', () => sdk.sync());
await sdk.sync();Clear cache
// Clear all locally cached data for one business/tenant (records, blobs, tags, reports, cases)
await sdk.clearDbForBid(BID);To run without any local persistence, construct the client with cache: false.
11. Electron Setup
IndexedDB works natively in the Electron renderer process. Only the transport differs:
import { MedicalRecordsClient, TransportMode } from '@eka-care/medical-records-ts-sdk';
// Electron
new MedicalRecordsClient({ transport: TransportMode.Bridge, defaultHeaders: {...} });
// Browser
new MedicalRecordsClient({ transport: TransportMode.Native, 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.
12. Error Handling
import {
EkaCareApiError,
UploadFailedError,
StorageLimitExceededError,
} from '@eka-care/medical-records-ts-sdk';
try {
await sdk.addDocument({ ... });
} 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 StorageLimitExceededError) {
// Server rejected the upload — the account's storage limit is reached (HTTP 403).
// The local temp record is removed automatically; no retry is possible.
console.error('Account storage limit reached', err);
} else if (err instanceof EkaCareApiError) {
console.error(err.message, err.statusCode, err.responseBody);
}
}editDocument, deleteDocument, createCase, updateCase, deleteCase never throw on network failure — local state is always updated and server sync retries automatically.
13. Document Types
Document types are opaque string codes owned by the server/your app; the SDK treats them as plain strings (it no longer ships label constants). Commonly used codes:
| Code | Label |
|---|---|
| ps | Prescription |
| lr | Lab Report |
| dc | Discharge Summary |
| vc | Vaccine Certificate |
| in | Insurance |
| iv | Invoice |
| sc | Scan |
| op | Other |
Pass a code wherever a document type is expected — e.g. filter: { documentType: 'lr' },
batchRequests: [{ documentType: 'lr', ... }], editDocument({ data: { documentType: 'ps' } }).
Map codes to display labels in your own app (see packages/ui for an example).
14. 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 LINKED_OIDS = []; // optional linked/dependent OIDs
export const BID = 'your-business-id';
# Start
cd packages/ui && npm run dev
# Open https://test.eka.care:5173Building
npm run build # TypeScript → dist/Publishing
npm version patch
npm publish --access public