npm package: @fhirust/sdk
Description: TypeScript SDK for building FHIRust WASM plugins
Latest version: 0.4.2

# @fhirust/sdk

> TypeScript SDK for building FHIRust WASM plugins

[![npm version](https://badge.fury.io/js/@fhirust%2Fsdk.svg)](https://www.npmjs.com/package/@fhirust/sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Build powerful FHIR resource validation, transformation, and enrichment plugins for [FHIRust](https://github.com/wei6bin/fhirust) using TypeScript and WebAssembly.

---

## 📦 Installation

```bash
npm install @fhirust/sdk @bytecodealliance/componentize-js
```

**Requirements:**
- Node.js 18+ or Bun
- TypeScript 5.0+
- `@bytecodealliance/componentize-js` >= 0.14.0 (0.19+ recommended for security fixes)

---

## 🚀 Quick Start

### 1. Create a new plugin

```typescript
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";

const plugin = new FhirPlugin("my-validator", "1.0.0");

// Validate Patient before creation
plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
    if (!patient.name || patient.name.length === 0) {
        return reject("Patient must have at least one name");
    }
    return patient;
});

// Export plugin hooks
const pluginExports = plugin.exports();
export const lifecycle = pluginExports.lifecycle;
export const hooks = pluginExports.hooks;
```

### 2. Build the plugin

```bash
# Using the SDK CLI
npx fhirust-sdk build

# Or manually with componentize-js
npx jco componentize src/index.ts -o dist/plugin.wasm \
  --wit wit/plugin.wit \
  --world-name fhirust-plugin
```

### 3. Deploy to FHIRust server

```toml
# config/server.toml
[[plugins.plugins]]
name = "my-validator"
path = "./plugins/my-validator.wasm"
enabled = true
```

---

## ✨ Features

- **Type-safe FHIR resources** — Full TypeScript types for FHIR R4 resources
- **Hook-based architecture** — Intercept and modify resources before/after CRUD operations
- **Built-in FHIR client** — Make authenticated API calls to the FHIR server
- **HTTP client** — Fetch data from external APIs with allowlist control
- **Event emission** — Send async events for logging, webhooks, and integrations
- **WebAssembly runtime** — Sandboxed execution with fine-grained permissions
- **Testing utilities** — Built-in test harness for unit testing plugins

---

## 📚 API Reference

### Core Classes

#### `FhirPlugin`

Main plugin class for registering hooks.

```typescript
import { FhirPlugin } from "@fhirust/sdk";

const plugin = new FhirPlugin(name: string, version: string);
```

**Methods:**

- `beforeCreate<T>(resourceType, handler)` — Hook before resource creation
- `afterCreate<T>(resourceType, handler)` — Hook after resource creation
- `beforeRead(resourceType, handler)` — Hook before resource read
- `afterRead<T>(resourceType, handler)` — Hook after resource read
- `beforeUpdate<T>(resourceType, handler)` — Hook before resource update
- `afterUpdate<T>(resourceType, handler)` — Hook after resource update
- `beforeDelete(resourceType, handler)` — Hook before resource deletion
- `afterDelete(resourceType, handler)` — Hook after resource deletion
- `exports()` — Export plugin metadata and hook executor

### Helper Functions

#### `reject(message: string): never`

Reject a hook operation with an error message.

```typescript
if (!patient.birthDate) {
  return reject("Patient birth date is required");
}
```

#### `outcome(issues: OperationOutcomeIssue[]): OperationOutcome`

Create a FHIR OperationOutcome for complex validation errors.

```typescript
return outcome([
  { severity: "error", code: "required", diagnostics: "Missing name" },
  { severity: "warning", code: "business-rule", diagnostics: "Unusual age" }
]);
```

### FHIR Client

Access the FHIR server from within your plugin using `plugin.fhir`.

```typescript
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
  // Search for duplicates
  const bundle = await plugin.fhir.search("Patient", {
    identifier: patient.identifier?.[0]?.value
  });

  if (bundle.total && bundle.total > 0) {
    return reject("Duplicate patient identifier");
  }

  return patient;
});
```

**Methods:**
- `plugin.fhir.search(resourceType, params)` — Search for resources
- `plugin.fhir.read(resourceType, id)` — Read a resource by ID
- `plugin.fhir.create(resourceType, resource)` — Create a new resource
- `plugin.fhir.update(resourceType, id, resource)` — Update a resource
- `plugin.fhir.delete(resourceType, id)` — Delete a resource

### HTTP Client

Make HTTP requests to external APIs using `plugin.http` (requires `http.fetch` permission).

```typescript
plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
  // Verify insurance eligibility via external API
  const response = await plugin.http.post("https://api.insurance.com/verify", {
    body: { memberId: patient.identifier?.[0]?.value }
  });

  if (!response.ok) {
    return reject("Insurance verification failed");
  }

  return patient;
});
```

### Event Emitter

Emit events for async processing using `plugin.events`.

```typescript
plugin.afterCreate<Patient>("Patient", (patient, ctx) => {
  plugin.events.emit("patient.created", {
    id: patient.id,
    name: patient.name?.[0]?.family
  });

  return patient;
});
```

### Logger

Structured logging with severity levels using `plugin.log`.

```typescript
plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
  plugin.log.info(`Validating patient: ${patient.id}`);
  plugin.log.warn(`Missing phone number for patient: ${patient.id}`);
  plugin.log.error(`Validation failed for patient: ${patient.id}`);

  return patient;
});
```

**Note:** Logger methods accept only string messages. To include structured data, serialize it in the message string using template literals or `JSON.stringify()`.

---

## 💡 Examples

### Example 1: Patient Name Validator

```typescript
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";

const plugin = new FhirPlugin("name-validator", "1.0.0");

plugin.beforeCreate<Patient>("Patient", (patient) => {
  if (!patient.name || patient.name.length === 0) {
    return reject("Patient must have at least one name");
  }

  const hasFamily = patient.name.some(n => n.family);
  if (!hasFamily) {
    return reject("At least one name must include a family name");
  }

  return patient;
});

export const { lifecycle, hooks } = plugin.exports();
```

### Example 2: Auto-Tag Resources

```typescript
import { FhirPlugin } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";

const plugin = new FhirPlugin("auto-tagger", "1.0.0");

plugin.beforeCreate<Patient>("Patient", (patient) => {
  // Add validation tag
  patient.meta = patient.meta || {};
  patient.meta.tag = patient.meta.tag || [];
  patient.meta.tag.push({
    system: "http://example.org/tags",
    code: "validated",
    display: "Validated by Plugin"
  });

  return patient;
});

export const { lifecycle, hooks } = plugin.exports();
```

### Example 3: Duplicate Detection

```typescript
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";

const plugin = new FhirPlugin("duplicate-detector", "1.0.0");

plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
  const identifier = patient.identifier?.[0]?.value;
  if (!identifier) return patient;

  try {
    // Search for existing patients with same identifier
    const bundle = await plugin.fhir.search("Patient", {
      identifier: identifier
    });

    if (bundle.total && bundle.total > 0) {
      return reject(`Duplicate patient found with identifier: ${identifier}`);
    }

    return patient;
  } catch (error) {
    plugin.log.error(`Duplicate check failed: ${error}`);
    // Fail open: allow creation if duplicate check fails
    return patient;
  }
});

export const { lifecycle, hooks } = plugin.exports();
```

### Example 4: External API Integration

```typescript
import { FhirPlugin, reject } from "@fhirust/sdk";
import type { Patient } from "@fhirust/sdk/r4";

const plugin = new FhirPlugin("insurance-verifier", "1.0.0");

plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
  const memberId = patient.identifier?.find(
    i => i.system === "http://insurance.org"
  )?.value;

  if (memberId) {
    try {
      // Verify with external insurance API
      const response = await plugin.http.post(
        "https://api.insurance.com/verify",
        { body: { memberId } }
      );

      if (!response.ok) {
        return reject("Insurance verification failed");
      }

      plugin.log.info(`Insurance verified for member: ${memberId}`);
    } catch (error) {
      plugin.log.error(`Insurance verification error: ${error}`);
      return reject("Insurance verification service unavailable");
    }
  }

  return patient;
});

export const { lifecycle, hooks } = plugin.exports();
```

---

## 🧪 Testing

Use the built-in testing utilities:

```typescript
import { FhirPlugin } from "@fhirust/sdk";
import { createTestContext } from "@fhirust/sdk/testing";
import type { Patient } from "@fhirust/sdk/r4";
import { test } from "node:test";
import assert from "node:assert";

test("validates patient name", async () => {
  const plugin = new FhirPlugin("test", "1.0.0");

  plugin.beforeCreate<Patient>("Patient", (patient) => {
    if (!patient.name?.length) {
      return reject("Name required");
    }
    return patient;
  });

  const ctx = createTestContext();
  const patient: Patient = {
    resourceType: "Patient",
    name: [{ family: "Doe", given: ["John"] }]
  };

  const handler = plugin.getHandler("beforeCreate", "Patient");
  const result = await handler(patient, ctx);

  assert.equal(result.name[0].family, "Doe");
});
```

Run tests:

```bash
npm test
```

---

## 🔧 CLI Commands

The SDK includes a CLI for common tasks:

```bash
# Create a new plugin from template
npx fhirust-sdk init my-plugin

# Build plugin to WASM
npx fhirust-sdk build

# Run tests
npx fhirust-sdk test

# Validate plugin manifest
npx fhirust-sdk validate
```

---

## 📝 Configuration

### Plugin Permissions

Configure plugin permissions in `config/server.toml`:

```toml
[[plugins.plugins]]
name = "my-plugin"
path = "./plugins/my-plugin.wasm"
enabled = true
permissions = [
  "fhir.read",          # Read FHIR resources
  "fhir.write",         # Create/update FHIR resources
  "utils.log",          # Write to server logs
  "utils.emit-event",   # Emit async events
  "http.fetch"          # Make HTTP requests
]
http_allowlist = [
  "https://api.insurance.com/*",
  "https://api.eligibility.org/*"
]
```

---

## 🤝 Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.

---

## 📄 License

MIT © FHIRust Contributors

---

## 🔗 Resources

- **Documentation**: https://fhirust.dev/docs/plugins
- **GitHub**: https://github.com/wei6bin/fhirust
- **npm**: https://www.npmjs.com/package/@fhirust/sdk
- **Issues**: https://github.com/wei6bin/fhirust/issues

---

## 📌 Version History

See [CHANGELOG.md](./CHANGELOG.md) for release notes.