Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c7d4df6
Initial project setup with TypeScript SDK for OMOPHub API, including …
May 29, 2026
17138cf
Enhance OMOPHub SDK with new concepts and domains resources, update N…
May 30, 2026
9f6b495
Refactor concepts and domains methods for improved query handling and…
May 30, 2026
d819a88
Merge pull request #2 from OMOPHub/initial-version
alex-omophub May 30, 2026
6d3eeab
Enhance OMOPHub SDK with new hierarchy, mappings, and relationships r…
May 30, 2026
5752c73
Refactor mappings and search interfaces for improved validation and r…
May 30, 2026
4dbfaac
Merge pull request #3 from OMOPHub/search
alex-omophub May 30, 2026
82b3dd4
Update biome configuration to include e2e tests in linting and format…
May 31, 2026
c7dde2b
Enhance environment variable loading in e2e tests with improved parsi…
May 31, 2026
7972d55
Merge pull request #4 from OMOPHub/fhir
alex-omophub May 31, 2026
8f80afe
Release version 1.0.0 of the OMOPHub Node.js SDK. Introduced comprehe…
May 31, 2026
1422115
Update package.json to include exports field for module resolution an…
May 31, 2026
fb4031b
Update CI workflow to include 'develop' branch for push and pull requ…
May 31, 2026
1e570ac
Update CHANGELOG for version 1.0.0 release, enhance FHIR resolver to …
May 31, 2026
4eed1c8
Merge pull request #5 from OMOPHub/examples
alex-omophub May 31, 2026
bf1419d
Refactor auth and utility functions for improved error handling and d…
May 31, 2026
c6ca3fa
Enhance `parseRetryAfter` function to strictly validate HTTP-date for…
May 31, 2026
4c52b25
Enhance CI workflow by adding manual triggering capability and introd…
May 31, 2026
9842164
Refine CI workflow to enforce branch restrictions for integration job…
May 31, 2026
f040834
Implement retry logic for POST requests on 429 status codes by introd…
May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch: # Allow manual triggering

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]

steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm

- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run typecheck
- run: npm test

integration:
# Only run on push or manual dispatch *and* only on main/develop.
# The branch check is load-bearing for `workflow_dispatch`: the
# Actions UI lets you pick any ref, so without it a collaborator
# could push a modified workflow to a feature branch and fire it
# with `TEST_API_KEY` attached, bypassing PR review. `push` events
# are already gated by `on.push.branches`, but checking github.ref
# here keeps both event types under one rule.
if: |
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm

- run: npm ci
- run: npm run build
- name: Run e2e tests against live API
env:
OMOPHUB_API_KEY: ${{ secrets.TEST_API_KEY }}
run: npm run test:e2e
27 changes: 27 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Publish to npm

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: npm

- run: npm ci
- run: npm run build
- run: npm test
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules/
dist/
coverage/
.env
.env.local
.env.*.local
*.log
npm-debug.log*
.DS_Store
.idea/
.vscode/
*.swp
*.swo
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-05-31

### Added

- Initial scaffolding: `OMOPHub` client skeleton with API key + base URL resolution, env-var precedence, and constructor-time validation.
- TypeScript build (ES2022, NodeNext, strict mode).
- Biome v2 lint + format.
- Vitest test runner with v8 coverage and a 90% statements threshold.
- CI workflow on Node 22 and 24 (`build`, `lint`, `typecheck`, `test`).
- npm publish workflow triggered by GitHub release, with provenance attestation.
- HTTP layer: `get` / `post` / `patch` / `put` / `delete` methods on `OMOPHub` with retry on 429 + 502/503/504 + network errors. Full-jitter exponential backoff (500ms → 8s), `Retry-After` honoured up to 60s.
- Discriminated `Response<T> = { data, error, meta, headers }` return type - errors never throw from network paths.
- 16-code `OMOPHUB_ERROR_CODE_KEY` union with stable codes mapped from HTTP status + server-provided error codes.
- `OMOPHubError` (thrown only on constructor misuse) and `OMOPHubIteratorError` (thrown from future async iterators).
- Common request-option interfaces: `PerCallOptions`, `GetOptions`, `PostOptions` (with `idempotencyKey`), `PatchOptions`, `PutOptions`, `DeleteOptions`.
- Pagination types: `PaginationOptions`, `PaginationMeta`, `PaginatedData<T>`.
- Vocab-release mixin + utility types (`RequireAtLeastOne`, `RequireExactlyOne`).
- Query builder: camelCase → snake_case, array → comma-join, null/undefined dropped.
- Envelope unwrap: tolerates both `{ success, data, meta }` and raw payload bodies.
- AbortSignal composition: client timeout + caller signal merged via `AbortSignal.any`; caller aborts propagate as thrown `AbortError`, timeouts return `timeout_error`.
- `X-Vocab-Version` header injection when `vocabVersion` option is set.
- First resource: `client.vocabularies.list()` with snake_case query serialisation, pagination, and error mapping.
- Test fixtures (`DIABETES_CONCEPT_ID` etc.) and mock-fetch helpers for `vi.fn`-based testing without external mock libraries.

### Added (resources)

- `client.concepts` - 7 methods: `get`, `getByCode`, `batch`, `suggest`, `related`, `relationships`, `recommended`. `concepts.get(0)` accepts the OMOP unmapped sentinel (R-SDK bug fix). `batch` validates 1–100 IDs synthetically; `recommended` validates `conceptIds` ≤ 100, `relationshipTypes` ≤ 20, `vocabularyIds`/`domainIds` ≤ 50.
- `client.vocabularies` extended with 6 methods: `get`, `stats`, `domainStats`, `domains` (vocab-scoped), `conceptClasses`, `concepts`.
- `client.domains` - 2 methods: `list`, `concepts`.
- `client.search` - 11 methods: `basic`, `basicIter`, `basicAll`, `advanced`, `autocomplete`, `semantic`, `semanticIter`, `semanticAll`, `bulkBasic`, `bulkSemantic`, `similar`. `bulkBasic` validates 1–50 searches; `bulkSemantic` validates 1–25; `similar` enforces XOR of `conceptId`/`conceptName`/`query` both at the TS type level (discriminated union) and at runtime.
- `client.hierarchy` - 3 methods: `get` (flat or graph format), `ancestors`, `descendants`. Server caps `maxLevels` at 20.
- `client.relationships` - 2 methods: `get` (shares wire endpoint with `concepts.relationships` - kept as parallel discoverable surface), `types`.
- `client.mappings` - 2 methods: `get` and `map`. `map` enforces XOR of `sourceConcepts` vs `sourceCodes` at both type and runtime levels. `vocabRelease` is routed to the `?vocab_release=` query parameter rather than the JSON body (matches Python SDK convention). JSDoc documents the Procedure-domain vocabulary priority chain (SNOMED → LOINC → CPT4 → HCPCS → ICD10PCS → ICD9Proc → OPCS4 → OMOP Extension).
- `ConceptHierarchyNode` extended with `domain_id`, `concept_class_id`, `standard_concept` optional fields - now matches Python's `HierarchyConcept` and is re-exported as `HierarchyConcept`/`Ancestor`/`Descendant` from the hierarchy module.
- `ConceptRelationship` re-exported as `Relationship` from the relationships module - kept in sync via type alias.
- `client.fhir` - 3 methods: `resolve` (accepts both flat `{ system, code }` and nested `{ coding: {...} }` forms, mirroring the Python SDK's `_extract_coding`), `resolveBatch` (1–100 codings), `resolveCodeableConcept` (1–20 codings). All three validate synthetically before issuing requests.
- Standalone helpers (no client required):
- `omophubFhirUrl(version)` - returns the URL of OMOPHub's hosted FHIR Terminology Service (`'r4'` default, also `'r4b'`, `'r5'`, `'r6'`).
- `getApiKey()`, `setApiKey(key)`, `hasApiKey()` - env-backed helpers reading `OMOPHUB_API_KEY` from `process.env`. `setApiKey` throws `OMOPHubError` on edge runtimes that lack a writable `process.env`.
- FHIR `Coding` type uses camelCase (`userSelected`, `vocabularyId`) to match the FHIR JSON spec - converted to snake_case at the wire via `toSnakeCaseKeys`.
- README polish: install + config table + per-resource usage examples for all 8 resources + error-handling guide + async-iterator guide + Python/R migration table.

### SDK surface at v1.0.0 - 37 methods across 8 resources

```
client.concepts - get, getByCode, batch, suggest, related, relationships, recommended (7)
client.search - basic, basicIter, basicAll, advanced, autocomplete, semantic,
semanticIter, semanticAll, bulkBasic, bulkSemantic, similar (11)
client.vocabularies - list, get, stats, domainStats, domains, conceptClasses, concepts (7)
client.domains - list, concepts (2)
client.hierarchy - get, ancestors, descendants (3)
client.relationships - get, types (2)
client.mappings - get, map (2)
client.fhir - resolve, resolveBatch, resolveCodeableConcept (3)
Σ = 37

Standalone: omophubFhirUrl, getApiKey, setApiKey, hasApiKey
```

<!-- Reference-style version links. Update when the v1.0.0 tag is cut. -->
[Unreleased]: https://github.com/OMOPHub/omophub-node/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/OMOPHub/omophub-node/releases/tag/v1.0.0

230 changes: 230 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Contributing to OMOPHub Node.js SDK

First off, thank you for considering contributing to OMOPHub!

## How Can I Contribute?

### Reporting Bugs

Before creating bug reports, please check the [existing issues](https://github.com/OMOPHub/omophub-node/issues) to avoid duplicates.

When creating a bug report, please include:

- **Node version** (`node --version`)
- **Package manager + version** (`npm --version` / `pnpm --version` / `bun --version`)
- **SDK version** (`npm ls @omophub/omophub-node`)
- **Runtime** (Node, Deno, Bun, Cloudflare Workers, Vercel Edge, browser)
- **Operating system**
- **Minimal code example** that reproduces the issue
- **Full error output** - `error.name`, `error.statusCode`, `error.message`, and `error.requestId` if available
- **Expected vs actual behavior**

If you're seeing a transport-level failure, include the response headers - especially `x-request-id` - so we can correlate against server logs.

### Suggesting Features

Feature requests are welcome! Please open an issue with:

- Clear description of the feature
- Use case: why would this be useful?
- Proposed TypeScript surface (option keys, return shape)
- Possible implementation approach (optional)

### Pull Requests

1. **Fork the repository** and create your branch from `main`
2. **Install dependencies:**
```bash
git clone https://github.com/YOUR_USERNAME/omophub-node.git
cd omophub-node
npm install
```
3. **Make your changes** with clear, descriptive commits
4. **Add tests** for new functionality - both unit tests (`test/`) and, when the change touches the wire, integration tests (`e2e/`)
5. **Run the full validation suite:**
```bash
npm run typecheck
npm run lint
npm test
npm run build
```
6. **Run integration tests** if your change affects request/response shapes:
```bash
OMOPHUB_API_KEY=oh_xxx npm run test:e2e
```
7. **Update documentation** - `README.md`, `CHANGELOG.md` under `## [Unreleased]`, and any relevant JSDoc
8. **Submit a pull request** with a clear description

## Development Setup

### Prerequisites

- **Node.js 22+** (see `.nvmrc`)
- **npm 10+** (ships with Node 22)

### Installation

```bash
# Clone your fork
git clone https://github.com/YOUR_USERNAME/omophub-node.git
cd omophub-node

# Use the pinned Node version
nvm use # or `fnm use`

# Install dependencies
npm install
```

For integration tests against the live API, create a `.env` file at the repo root:

```bash
echo "export OMOPHUB_API_KEY=oh_xxx" > .env
```

`.env` is gitignored - never commit your key.

### Running Tests

```bash
# Unit tests (fast, fully mocked - no network)
npm test

# Watch mode for active development
npm run test:watch

# With coverage report
npm run test:coverage

# Integration tests against the live API (requires OMOPHUB_API_KEY)
set -a && source .env && set +a
npm run test:e2e

# Run a single test file
npx vitest run test/concepts/concepts.test.ts

# Run tests matching a pattern
npx vitest run -t "bulk"
```

Unit tests live in `test/` and use `vitest` with `vi.fn()`-based mock fetches - no external mock libraries. Integration tests live in `e2e/` and exercise the SDK against `api.omophub.com/v1`; they tolerate rate limits and known slow paths via the helpers in `e2e/_helpers.ts`.

### Code Style

We use:

- **Biome v2** for linting and formatting
- **TypeScript strict mode** (no `any`, no implicit returns, no unused locals)

```bash
# Check lint + format
npm run lint

# Auto-fix lint + format
npm run lint:fix

# Format only
npm run format

# Type-check (src + e2e)
npm run typecheck
```

Run `npm run lint:fix && npm run typecheck` before pushing. CI runs the same checks.

### Running Examples

The `examples/` directory contains runnable scripts you can use to smoke-test your changes end-to-end:

```bash
set -a && source .env && set +a
npx tsx examples/basic-usage.ts
npx tsx examples/fhir-resolver.ts
```

If your change affects a public method, add or update an example.

## Project Structure

```
omophub-node/
├── src/
│ ├── index.ts # Public API exports
│ ├── client.ts # OMOPHub client + HTTP dispatch + retry
│ ├── errors.ts # OMOPHubError, OMOPHubIteratorError, error codes
│ ├── version.ts # SDK version (auto-bumped)
│ ├── interfaces.ts # Shared request/response/option types
│ ├── auth/ # API key env helpers
│ ├── common/ # syntheticError, pagination, toSnakeCaseKeys, …
│ ├── concepts/
│ │ ├── concepts.ts # Concepts resource methods
│ │ └── interfaces/ # Per-method option + response types
│ ├── search/
│ ├── hierarchy/
│ ├── relationships/
│ ├── mappings/
│ ├── vocabularies/
│ ├── domains/
│ └── fhir/
├── test/ # Unit tests (mock-fetch, no network)
├── e2e/ # Integration tests (live api.omophub.com)
├── examples/ # Runnable example scripts
├── package.json
├── tsconfig.json # src build config
├── tsconfig.e2e.json # e2e typecheck config
├── tsconfig.examples.json # examples typecheck config
├── biome.json # Lint + format rules
├── vitest.config.ts # Unit test runner
└── vitest.e2e.config.ts # Integration test runner (fileParallelism: false)
```

## SDK Conventions

A few patterns are load-bearing - please follow them when adding new methods:

- **Discriminated returns, not exceptions.** Every method returns `Promise<{ data, error, meta, headers }>`. The only exceptions the SDK throws are `OMOPHubError` (constructor misuse) and `OMOPHubIteratorError` (`*Iter` async-generator page failures). Network errors, 404s, 429s, validation failures - all returned as `error` values.
- **camelCase TypeScript surface, snake_case wire.** Request options take camelCase keys (`vocabularyIds`, `pageSize`); they're converted to snake_case at the request boundary via `toSnakeCaseKeys`. Response types use snake_case to match the wire - never client-translated.
- **Synthetic validation when feasible.** Reject obviously-bad inputs (empty arrays, out-of-range counts, missing required fields) with `syntheticError(...)` before making a network call. Match the wire-shape of a real server error so callers' error-handling code doesn't have to branch.
- **Positional path args + merged options.** Prefer `client.concepts.get(201826, { includeRelationships: true })` over `({ conceptId: 201826, ... })`. Path params are positional; everything else goes in a single options object.
- **Idempotency-key retries.** GET/HEAD/OPTIONS/PUT/DELETE retry automatically on transient failures. POST/PATCH only retry when the caller sets `{ idempotencyKey: '...' }` - never silently retry a non-idempotent write.
- **Pagination metadata on the outer envelope.** `meta.pagination` lives on the response, never inside `data`. Add `*Iter` (async generator) and `*All` (eager collector with error accumulation) variants for any new paginated resource.

When the live API returns a shape that doesn't match the existing types, **fix the type to match the wire** - don't transform on the response path. Log the discovery in `CHANGELOG.md` so the wire-drift history stays traceable.

## Commit Messages

We follow [Conventional Commits](https://www.conventionalcommits.org/):

- `feat:` New feature
- `fix:` Bug fix
- `docs:` Documentation changes
- `test:` Adding or updating tests
- `refactor:` Code refactoring
- `chore:` Maintenance tasks

Examples:
```
feat: add semantic search endpoint
fix: handle rate limit errors correctly
docs: update README with new examples
test: add integration tests for batch concept lookup
```

## Release Process

Releases are cut by maintainers via GitHub Releases - **do not bump the version in `package.json` in your PR**. The publish workflow (`.github/workflows/publish.yml`) runs on release-published, publishes to npm with provenance attestation, and tags the registry version.

For each PR, add a bullet under `## [Unreleased]` in `CHANGELOG.md` describing the user-visible change.

## Questions?

- Open a [GitHub Discussion](https://github.com/OMOPHub/omophub-node/discussions)
- Email: support@omophub.com

## License

By contributing, you agree that your contributions will be licensed under the MIT License.

---

Thank you for helping make OMOPHub better!
Loading