@fhirust/sdk 0.3.0 โ†’ 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +431 -0
  2. package/package.json +4 -4
package/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # @fhirust/sdk
2
+
3
+ > TypeScript SDK for building FHIRust WASM plugins
4
+
5
+ [![npm version](https://badge.fury.io/js/@fhirust%2Fsdk.svg)](https://www.npmjs.com/package/@fhirust/sdk)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Build powerful FHIR resource validation, transformation, and enrichment plugins for [FHIRust](https://github.com/wei6bin/fhirust) using TypeScript and WebAssembly.
9
+
10
+ ---
11
+
12
+ ## ๐Ÿ“ฆ Installation
13
+
14
+ ```bash
15
+ npm install @fhirust/sdk @bytecodealliance/componentize-js
16
+ ```
17
+
18
+ **Requirements:**
19
+ - Node.js 18+ or Bun
20
+ - TypeScript 5.0+
21
+ - `@bytecodealliance/componentize-js` >= 0.14.0 (0.19+ recommended for security fixes)
22
+
23
+ ---
24
+
25
+ ## ๐Ÿš€ Quick Start
26
+
27
+ ### 1. Create a new plugin
28
+
29
+ ```typescript
30
+ import { FhirPlugin, reject } from "@fhirust/sdk";
31
+ import type { Patient } from "@fhirust/sdk/r4";
32
+
33
+ const plugin = new FhirPlugin("my-validator", "1.0.0");
34
+
35
+ // Validate Patient before creation
36
+ plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
37
+ if (!patient.name || patient.name.length === 0) {
38
+ return reject("Patient must have at least one name");
39
+ }
40
+ return patient;
41
+ });
42
+
43
+ // Export plugin hooks
44
+ export const { getMetadata, executeHook } = plugin.exports();
45
+ ```
46
+
47
+ ### 2. Build the plugin
48
+
49
+ ```bash
50
+ # Using the SDK CLI
51
+ npx fhirust-sdk build
52
+
53
+ # Or manually with componentize-js
54
+ npx jco componentize src/index.ts -o dist/plugin.wasm \
55
+ --wit wit/plugin.wit \
56
+ --world-name fhirust-plugin
57
+ ```
58
+
59
+ ### 3. Deploy to FHIRust server
60
+
61
+ ```toml
62
+ # config/server.toml
63
+ [[plugins.plugins]]
64
+ name = "my-validator"
65
+ path = "./plugins/my-validator.wasm"
66
+ enabled = true
67
+ ```
68
+
69
+ ---
70
+
71
+ ## โœจ Features
72
+
73
+ - **Type-safe FHIR resources** โ€” Full TypeScript types for FHIR R4 resources
74
+ - **Hook-based architecture** โ€” Intercept and modify resources before/after CRUD operations
75
+ - **Built-in FHIR client** โ€” Make authenticated API calls to the FHIR server
76
+ - **HTTP client** โ€” Fetch data from external APIs with allowlist control
77
+ - **Event emission** โ€” Send async events for logging, webhooks, and integrations
78
+ - **WebAssembly runtime** โ€” Sandboxed execution with fine-grained permissions
79
+ - **Testing utilities** โ€” Built-in test harness for unit testing plugins
80
+
81
+ ---
82
+
83
+ ## ๐Ÿ“š API Reference
84
+
85
+ ### Core Classes
86
+
87
+ #### `FhirPlugin`
88
+
89
+ Main plugin class for registering hooks.
90
+
91
+ ```typescript
92
+ import { FhirPlugin } from "@fhirust/sdk";
93
+
94
+ const plugin = new FhirPlugin(name: string, version: string);
95
+ ```
96
+
97
+ **Methods:**
98
+
99
+ - `beforeCreate<T>(resourceType, handler)` โ€” Hook before resource creation
100
+ - `afterCreate<T>(resourceType, handler)` โ€” Hook after resource creation
101
+ - `beforeRead(resourceType, handler)` โ€” Hook before resource read
102
+ - `afterRead<T>(resourceType, handler)` โ€” Hook after resource read
103
+ - `beforeUpdate<T>(resourceType, handler)` โ€” Hook before resource update
104
+ - `afterUpdate<T>(resourceType, handler)` โ€” Hook after resource update
105
+ - `beforeDelete(resourceType, handler)` โ€” Hook before resource deletion
106
+ - `afterDelete(resourceType, handler)` โ€” Hook after resource deletion
107
+ - `exports()` โ€” Export plugin metadata and hook executor
108
+
109
+ ### Helper Functions
110
+
111
+ #### `reject(message: string): never`
112
+
113
+ Reject a hook operation with an error message.
114
+
115
+ ```typescript
116
+ if (!patient.birthDate) {
117
+ return reject("Patient birth date is required");
118
+ }
119
+ ```
120
+
121
+ #### `outcome(issues: OperationOutcomeIssue[]): OperationOutcome`
122
+
123
+ Create a FHIR OperationOutcome for complex validation errors.
124
+
125
+ ```typescript
126
+ return outcome([
127
+ { severity: "error", code: "required", diagnostics: "Missing name" },
128
+ { severity: "warning", code: "business-rule", diagnostics: "Unusual age" }
129
+ ]);
130
+ ```
131
+
132
+ ### FHIR Client
133
+
134
+ Access the FHIR server from within your plugin.
135
+
136
+ ```typescript
137
+ plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
138
+ // Search for duplicates
139
+ const bundle = await ctx.fhir.search("Patient", {
140
+ identifier: patient.identifier?.[0]?.value
141
+ });
142
+
143
+ if (bundle.total > 0) {
144
+ return reject("Duplicate patient identifier");
145
+ }
146
+
147
+ return patient;
148
+ });
149
+ ```
150
+
151
+ **Methods:**
152
+ - `fhir.search(resourceType, params)` โ€” Search for resources
153
+ - `fhir.read(resourceType, id)` โ€” Read a resource by ID
154
+ - `fhir.create(resourceType, resource)` โ€” Create a new resource
155
+ - `fhir.update(resourceType, id, resource)` โ€” Update a resource
156
+ - `fhir.delete(resourceType, id)` โ€” Delete a resource
157
+
158
+ ### HTTP Client
159
+
160
+ Make HTTP requests to external APIs (requires `http.fetch` permission).
161
+
162
+ ```typescript
163
+ plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
164
+ // Verify insurance eligibility via external API
165
+ const response = await ctx.http.post("https://api.insurance.com/verify", {
166
+ body: { memberId: patient.identifier?.[0]?.value }
167
+ });
168
+
169
+ if (!response.ok) {
170
+ return reject("Insurance verification failed");
171
+ }
172
+
173
+ return patient;
174
+ });
175
+ ```
176
+
177
+ ### Event Emitter
178
+
179
+ Emit events for async processing.
180
+
181
+ ```typescript
182
+ plugin.afterCreate<Patient>("Patient", (patient, ctx) => {
183
+ ctx.events.emit("patient.created", {
184
+ id: patient.id,
185
+ name: patient.name?.[0]?.family
186
+ });
187
+
188
+ return patient;
189
+ });
190
+ ```
191
+
192
+ ### Logger
193
+
194
+ Structured logging with severity levels.
195
+
196
+ ```typescript
197
+ plugin.beforeCreate<Patient>("Patient", (patient, ctx) => {
198
+ ctx.logger.info("Validating patient", { id: patient.id });
199
+ ctx.logger.warn("Missing phone number", { id: patient.id });
200
+ ctx.logger.error("Validation failed", { id: patient.id });
201
+
202
+ return patient;
203
+ });
204
+ ```
205
+
206
+ ---
207
+
208
+ ## ๐Ÿ’ก Examples
209
+
210
+ ### Example 1: Patient Name Validator
211
+
212
+ ```typescript
213
+ import { FhirPlugin, reject } from "@fhirust/sdk";
214
+ import type { Patient } from "@fhirust/sdk/r4";
215
+
216
+ const plugin = new FhirPlugin("name-validator", "1.0.0");
217
+
218
+ plugin.beforeCreate<Patient>("Patient", (patient) => {
219
+ if (!patient.name || patient.name.length === 0) {
220
+ return reject("Patient must have at least one name");
221
+ }
222
+
223
+ const hasFamily = patient.name.some(n => n.family);
224
+ if (!hasFamily) {
225
+ return reject("At least one name must include a family name");
226
+ }
227
+
228
+ return patient;
229
+ });
230
+
231
+ export const { getMetadata, executeHook } = plugin.exports();
232
+ ```
233
+
234
+ ### Example 2: Auto-Tag Resources
235
+
236
+ ```typescript
237
+ import { FhirPlugin } from "@fhirust/sdk";
238
+ import type { Patient } from "@fhirust/sdk/r4";
239
+
240
+ const plugin = new FhirPlugin("auto-tagger", "1.0.0");
241
+
242
+ plugin.beforeCreate<Patient>("Patient", (patient) => {
243
+ // Add validation tag
244
+ patient.meta = patient.meta || {};
245
+ patient.meta.tag = patient.meta.tag || [];
246
+ patient.meta.tag.push({
247
+ system: "http://example.org/tags",
248
+ code: "validated",
249
+ display: "Validated by Plugin"
250
+ });
251
+
252
+ return patient;
253
+ });
254
+
255
+ export const { getMetadata, executeHook } = plugin.exports();
256
+ ```
257
+
258
+ ### Example 3: Duplicate Detection
259
+
260
+ ```typescript
261
+ import { FhirPlugin, reject } from "@fhirust/sdk";
262
+ import type { Patient } from "@fhirust/sdk/r4";
263
+
264
+ const plugin = new FhirPlugin("duplicate-detector", "1.0.0");
265
+
266
+ plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
267
+ const identifier = patient.identifier?.[0]?.value;
268
+ if (!identifier) return patient;
269
+
270
+ // Search for existing patients with same identifier
271
+ const bundle = await ctx.fhir.search("Patient", {
272
+ identifier: identifier
273
+ });
274
+
275
+ if (bundle.total && bundle.total > 0) {
276
+ return reject(`Duplicate patient found with identifier: ${identifier}`);
277
+ }
278
+
279
+ return patient;
280
+ });
281
+
282
+ export const { getMetadata, executeHook } = plugin.exports();
283
+ ```
284
+
285
+ ### Example 4: External API Integration
286
+
287
+ ```typescript
288
+ import { FhirPlugin, reject } from "@fhirust/sdk";
289
+ import type { Patient } from "@fhirust/sdk/r4";
290
+
291
+ const plugin = new FhirPlugin("insurance-verifier", "1.0.0");
292
+
293
+ plugin.beforeCreate<Patient>("Patient", async (patient, ctx) => {
294
+ const memberId = patient.identifier?.find(
295
+ i => i.system === "http://insurance.org"
296
+ )?.value;
297
+
298
+ if (memberId) {
299
+ // Verify with external insurance API
300
+ const response = await ctx.http.post(
301
+ "https://api.insurance.com/verify",
302
+ { body: { memberId } }
303
+ );
304
+
305
+ if (!response.ok) {
306
+ return reject("Insurance verification failed");
307
+ }
308
+
309
+ ctx.logger.info("Insurance verified", { memberId });
310
+ }
311
+
312
+ return patient;
313
+ });
314
+
315
+ export const { getMetadata, executeHook } = plugin.exports();
316
+ ```
317
+
318
+ ---
319
+
320
+ ## ๐Ÿงช Testing
321
+
322
+ Use the built-in testing utilities:
323
+
324
+ ```typescript
325
+ import { FhirPlugin } from "@fhirust/sdk";
326
+ import { createTestContext } from "@fhirust/sdk/testing";
327
+ import type { Patient } from "@fhirust/sdk/r4";
328
+ import { test } from "node:test";
329
+ import assert from "node:assert";
330
+
331
+ test("validates patient name", async () => {
332
+ const plugin = new FhirPlugin("test", "1.0.0");
333
+
334
+ plugin.beforeCreate<Patient>("Patient", (patient) => {
335
+ if (!patient.name?.length) {
336
+ return reject("Name required");
337
+ }
338
+ return patient;
339
+ });
340
+
341
+ const ctx = createTestContext();
342
+ const patient: Patient = {
343
+ resourceType: "Patient",
344
+ name: [{ family: "Doe", given: ["John"] }]
345
+ };
346
+
347
+ const handler = plugin.getHandler("beforeCreate", "Patient");
348
+ const result = await handler(patient, ctx);
349
+
350
+ assert.equal(result.name[0].family, "Doe");
351
+ });
352
+ ```
353
+
354
+ Run tests:
355
+
356
+ ```bash
357
+ npm test
358
+ ```
359
+
360
+ ---
361
+
362
+ ## ๐Ÿ”ง CLI Commands
363
+
364
+ The SDK includes a CLI for common tasks:
365
+
366
+ ```bash
367
+ # Create a new plugin from template
368
+ npx fhirust-sdk init my-plugin
369
+
370
+ # Build plugin to WASM
371
+ npx fhirust-sdk build
372
+
373
+ # Run tests
374
+ npx fhirust-sdk test
375
+
376
+ # Validate plugin manifest
377
+ npx fhirust-sdk validate
378
+ ```
379
+
380
+ ---
381
+
382
+ ## ๐Ÿ“ Configuration
383
+
384
+ ### Plugin Permissions
385
+
386
+ Configure plugin permissions in `config/server.toml`:
387
+
388
+ ```toml
389
+ [[plugins.plugins]]
390
+ name = "my-plugin"
391
+ path = "./plugins/my-plugin.wasm"
392
+ enabled = true
393
+ permissions = [
394
+ "fhir.read", # Read FHIR resources
395
+ "fhir.write", # Create/update FHIR resources
396
+ "utils.log", # Write to server logs
397
+ "utils.emit-event", # Emit async events
398
+ "http.fetch" # Make HTTP requests
399
+ ]
400
+ http_allowlist = [
401
+ "https://api.insurance.com/*",
402
+ "https://api.eligibility.org/*"
403
+ ]
404
+ ```
405
+
406
+ ---
407
+
408
+ ## ๐Ÿค Contributing
409
+
410
+ Contributions are welcome! Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
411
+
412
+ ---
413
+
414
+ ## ๐Ÿ“„ License
415
+
416
+ MIT ยฉ FHIRust Contributors
417
+
418
+ ---
419
+
420
+ ## ๐Ÿ”— Resources
421
+
422
+ - **Documentation**: https://fhirust.dev/docs/plugins
423
+ - **GitHub**: https://github.com/wei6bin/fhirust
424
+ - **npm**: https://www.npmjs.com/package/@fhirust/sdk
425
+ - **Issues**: https://github.com/wei6bin/fhirust/issues
426
+
427
+ ---
428
+
429
+ ## ๐Ÿ“Œ Version History
430
+
431
+ See [CHANGELOG.md](./CHANGELOG.md) for release notes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fhirust/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "TypeScript SDK for building FHIRust WASM plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "license": "MIT",
45
45
  "peerDependencies": {
46
- "@bytecodealliance/componentize-js": ">=0.14.0"
46
+ "@bytecodealliance/componentize-js": ">=0.14.0 <1.0.0"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@bytecodealliance/componentize-js": {
@@ -51,9 +51,9 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "@bytecodealliance/componentize-js": "^0.14.0",
54
+ "@bytecodealliance/componentize-js": "^0.19.3",
55
55
  "@types/node": "^25.2.2",
56
56
  "tsx": "^4.21.0",
57
57
  "typescript": "^5.5.0"
58
58
  }
59
- }
59
+ }