@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.
- package/README.md +431 -0
- 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
|
+
[](https://www.npmjs.com/package/@fhirust/sdk)
|
|
6
|
+
[](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.
|
|
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.
|
|
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
|
+
}
|