vintasend-medplum 0.4.0
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 +506 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/medplum-adapter.d.ts +19 -0
- package/dist/medplum-adapter.js +49 -0
- package/dist/medplum-attachment-manager.d.ts +82 -0
- package/dist/medplum-attachment-manager.js +228 -0
- package/dist/medplum-backend.d.ts +86 -0
- package/dist/medplum-backend.js +831 -0
- package/dist/medplum-logger.d.ts +8 -0
- package/dist/medplum-logger.js +18 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# VintaSend Medplum Implementation
|
|
2
|
+
|
|
3
|
+
A complete VintaSend implementation using [Medplum](https://www.medplum.com/) as the backend, leveraging FHIR resources for notification management, file storage, and healthcare integration.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This implementation uses FHIR (Fast Healthcare Interoperability Resources) standards to store and manage notifications, making it ideal for healthcare applications that need to integrate notifications with patient care workflows.
|
|
8
|
+
|
|
9
|
+
### Key Components
|
|
10
|
+
|
|
11
|
+
- **MedplumNotificationBackend**: Stores notifications as FHIR `Communication` resources
|
|
12
|
+
- **MedplumNotificationAdapter**: Sends email notifications via Medplum's email API
|
|
13
|
+
- **MedplumAttachmentManager**: Manages file attachments using FHIR `Binary` and `Media` resources
|
|
14
|
+
- **MedplumLogger**: Simple console-based logger
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
### FHIR Resource Mapping
|
|
19
|
+
|
|
20
|
+
The implementation maps VintaSend concepts to FHIR resources:
|
|
21
|
+
|
|
22
|
+
#### Notifications → Communication Resources
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
{
|
|
26
|
+
resourceType: "Communication",
|
|
27
|
+
status: "in-progress", // PENDING_SEND
|
|
28
|
+
sent: "2024-01-15T10:00:00Z", // sendAfter
|
|
29
|
+
topic: { text: "Welcome!" }, // title
|
|
30
|
+
recipient: [{ reference: "Patient/123" }], // userId
|
|
31
|
+
payload: [{
|
|
32
|
+
contentString: "Hello {{name}}", // bodyTemplate
|
|
33
|
+
extension: [{
|
|
34
|
+
url: "http://vintasend.com/fhir/StructureDefinition/email-notification-subject",
|
|
35
|
+
valueString: "Welcome {{name}}" // subjectTemplate
|
|
36
|
+
}]
|
|
37
|
+
}],
|
|
38
|
+
note: [{ text: '{"userId": "123"}' }], // contextParameters
|
|
39
|
+
meta: {
|
|
40
|
+
tag: [
|
|
41
|
+
{ code: "notification" },
|
|
42
|
+
{ code: "user-welcome" }, // contextName
|
|
43
|
+
{ code: "EMAIL" } // notificationType
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### File Attachments → Binary + Media Resources
|
|
50
|
+
|
|
51
|
+
Files are stored using two FHIR resources:
|
|
52
|
+
|
|
53
|
+
1. **Binary Resource**: Stores the actual file data (base64 encoded)
|
|
54
|
+
2. **Media Resource**: Stores metadata and links to the Binary
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Binary resource
|
|
58
|
+
{
|
|
59
|
+
resourceType: "Binary",
|
|
60
|
+
contentType: "application/pdf",
|
|
61
|
+
data: "base64EncodedData..."
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Media resource
|
|
65
|
+
{
|
|
66
|
+
resourceType: "Media",
|
|
67
|
+
status: "completed",
|
|
68
|
+
content: {
|
|
69
|
+
contentType: "application/pdf",
|
|
70
|
+
url: "Binary/binary-id",
|
|
71
|
+
size: 12345,
|
|
72
|
+
title: "invoice.pdf"
|
|
73
|
+
},
|
|
74
|
+
identifier: [{
|
|
75
|
+
system: "http://vintasend.com/fhir/attachment-checksum",
|
|
76
|
+
value: "sha256-checksum"
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Status Mapping
|
|
82
|
+
|
|
83
|
+
| VintaSend Status | FHIR Communication Status |
|
|
84
|
+
|-----------------|---------------------------|
|
|
85
|
+
| PENDING_SEND | in-progress |
|
|
86
|
+
| SENT | completed |
|
|
87
|
+
| FAILED | stopped |
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install vintasend-medplum @medplum/core
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Setup
|
|
96
|
+
|
|
97
|
+
### Basic Configuration
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { MedplumClient } from '@medplum/core';
|
|
101
|
+
import { MedplumNotificationBackend } from 'vintasend-medplum';
|
|
102
|
+
import { MedplumNotificationAdapter } from 'vintasend-medplum';
|
|
103
|
+
import { MedplumAttachmentManager } from 'vintasend-medplum';
|
|
104
|
+
import { MedplumLogger } from 'vintasend-medplum';
|
|
105
|
+
import { PugTemplateRenderer } from 'vintasend-pug';
|
|
106
|
+
import { NotificationService } from 'vintasend';
|
|
107
|
+
|
|
108
|
+
// Initialize Medplum client
|
|
109
|
+
const medplum = new MedplumClient({
|
|
110
|
+
baseUrl: 'https://api.medplum.com',
|
|
111
|
+
clientId: 'your-client-id',
|
|
112
|
+
clientSecret: 'your-client-secret',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Create template renderer
|
|
116
|
+
const templateRenderer = new PugTemplateRenderer({
|
|
117
|
+
templatesDir: './templates',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Create notification adapter
|
|
121
|
+
const adapter = new MedplumNotificationAdapter(medplum, templateRenderer);
|
|
122
|
+
|
|
123
|
+
// Create backend
|
|
124
|
+
const backend = new MedplumNotificationBackend(medplum, {
|
|
125
|
+
emailNotificationSubjectExtensionUrl: 'http://your-domain.com/fhir/StructureDefinition/email-notification-subject',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Create attachment manager (optional)
|
|
129
|
+
const attachmentManager = new MedplumAttachmentManager(medplum);
|
|
130
|
+
|
|
131
|
+
// Create logger
|
|
132
|
+
const logger = new MedplumLogger();
|
|
133
|
+
|
|
134
|
+
// Initialize notification service
|
|
135
|
+
const notificationService = new NotificationService(
|
|
136
|
+
backend,
|
|
137
|
+
[adapter],
|
|
138
|
+
logger,
|
|
139
|
+
attachmentManager,
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### With Custom Configuration
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Custom extension URL for email subjects
|
|
147
|
+
const backend = new MedplumNotificationBackend(medplum, {
|
|
148
|
+
emailNotificationSubjectExtensionUrl: 'http://example.com/fhir/email-subject',
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Usage Examples
|
|
153
|
+
|
|
154
|
+
### Sending a Simple Notification
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Create a notification
|
|
158
|
+
await notificationService.createNotification({
|
|
159
|
+
userId: 'Patient/123',
|
|
160
|
+
notificationType: 'EMAIL',
|
|
161
|
+
contextName: 'user-welcome',
|
|
162
|
+
contextParameters: {
|
|
163
|
+
firstName: 'John',
|
|
164
|
+
lastName: 'Doe',
|
|
165
|
+
},
|
|
166
|
+
title: 'Welcome to our platform!',
|
|
167
|
+
bodyTemplate: 'Hello {{firstName}} {{lastName}}!',
|
|
168
|
+
subjectTemplate: 'Welcome {{firstName}}!',
|
|
169
|
+
sendAfter: new Date(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Process pending notifications
|
|
173
|
+
await notificationService.processPendingNotifications();
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Sending Notifications with Attachments
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { readFile } from 'fs/promises';
|
|
180
|
+
|
|
181
|
+
// Create notification with file attachments
|
|
182
|
+
await notificationService.createNotification({
|
|
183
|
+
userId: 'Patient/123',
|
|
184
|
+
notificationType: 'EMAIL',
|
|
185
|
+
contextName: 'lab-results',
|
|
186
|
+
contextParameters: {
|
|
187
|
+
patientName: 'John Doe',
|
|
188
|
+
},
|
|
189
|
+
title: 'Your lab results are ready',
|
|
190
|
+
bodyTemplate: 'Dear {{patientName}}, your lab results are attached.',
|
|
191
|
+
subjectTemplate: 'Lab Results - {{patientName}}',
|
|
192
|
+
sendAfter: new Date(),
|
|
193
|
+
attachments: [
|
|
194
|
+
{
|
|
195
|
+
file: await readFile('./lab-results.pdf'),
|
|
196
|
+
filename: 'lab-results.pdf',
|
|
197
|
+
contentType: 'application/pdf',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### One-Off Notifications
|
|
204
|
+
|
|
205
|
+
Send notifications to users without storing them in the system:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
await notificationService.createOneOffNotification({
|
|
209
|
+
emailOrPhone: 'patient@example.com',
|
|
210
|
+
firstName: 'Jane',
|
|
211
|
+
lastName: 'Smith',
|
|
212
|
+
notificationType: 'EMAIL',
|
|
213
|
+
contextName: 'appointment-reminder',
|
|
214
|
+
contextParameters: {
|
|
215
|
+
appointmentDate: '2024-02-01',
|
|
216
|
+
doctorName: 'Dr. Johnson',
|
|
217
|
+
},
|
|
218
|
+
title: 'Appointment Reminder',
|
|
219
|
+
bodyTemplate: 'Hi {{firstName}}, reminder for your appointment on {{appointmentDate}}.',
|
|
220
|
+
subjectTemplate: 'Appointment on {{appointmentDate}}',
|
|
221
|
+
sendAfter: new Date(),
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Managing Attachments
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Upload a file
|
|
229
|
+
const fileRecord = await attachmentManager.uploadFile(
|
|
230
|
+
buffer,
|
|
231
|
+
'document.pdf',
|
|
232
|
+
'application/pdf',
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Retrieve file metadata
|
|
236
|
+
const file = await attachmentManager.getFile(fileRecord.id);
|
|
237
|
+
|
|
238
|
+
// Get file data
|
|
239
|
+
const attachmentFile = attachmentManager.reconstructAttachmentFile(
|
|
240
|
+
file.storageMetadata,
|
|
241
|
+
);
|
|
242
|
+
const fileBuffer = await attachmentFile.read();
|
|
243
|
+
|
|
244
|
+
// Generate temporary URL
|
|
245
|
+
const url = await attachmentFile.url(3600); // 1 hour expiry
|
|
246
|
+
|
|
247
|
+
// Delete file
|
|
248
|
+
await attachmentManager.deleteFile(fileRecord.id);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Querying Notifications
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Get pending notifications
|
|
255
|
+
const pending = await notificationService.getPendingNotifications(0, 10);
|
|
256
|
+
|
|
257
|
+
// Get future notifications for a user
|
|
258
|
+
const future = await notificationService.getFutureNotificationsFromUser(
|
|
259
|
+
'Patient/123',
|
|
260
|
+
0,
|
|
261
|
+
10,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Get unread in-app notifications
|
|
265
|
+
const unread = await notificationService.filterInAppUnreadNotifications(
|
|
266
|
+
'Patient/123',
|
|
267
|
+
0,
|
|
268
|
+
10,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Mark as read
|
|
272
|
+
await notificationService.markAsRead(notificationId);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Canceling Notifications
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Cancel a scheduled notification
|
|
279
|
+
await notificationService.cancelNotification(notificationId);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Healthcare Integration
|
|
283
|
+
|
|
284
|
+
This implementation is designed for healthcare applications and integrates naturally with Medplum's FHIR-based infrastructure.
|
|
285
|
+
|
|
286
|
+
### Linking Notifications to Patients
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Create notification linked to a patient
|
|
290
|
+
await notificationService.createNotification({
|
|
291
|
+
userId: 'Patient/123', // FHIR Patient reference
|
|
292
|
+
notificationType: 'EMAIL',
|
|
293
|
+
contextName: 'medication-reminder',
|
|
294
|
+
contextParameters: {
|
|
295
|
+
medicationName: 'Aspirin',
|
|
296
|
+
dosage: '100mg',
|
|
297
|
+
},
|
|
298
|
+
// ...
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Linking Notifications to Practitioners
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
// Notify a healthcare provider
|
|
306
|
+
await notificationService.createNotification({
|
|
307
|
+
userId: 'Practitioner/456', // FHIR Practitioner reference
|
|
308
|
+
notificationType: 'EMAIL',
|
|
309
|
+
contextName: 'new-patient-alert',
|
|
310
|
+
// ...
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Searching Notifications by Resource
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// Get all notifications for a patient
|
|
318
|
+
const communications = await medplum.searchResources('Communication', {
|
|
319
|
+
_tag: 'notification',
|
|
320
|
+
recipient: 'Patient/123',
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Features
|
|
325
|
+
|
|
326
|
+
### ✅ Supported Features
|
|
327
|
+
|
|
328
|
+
- Email notifications via Medplum's email API
|
|
329
|
+
- File attachments using FHIR Binary and Media resources
|
|
330
|
+
- One-off notifications (no user account required)
|
|
331
|
+
- Scheduled notifications (send later)
|
|
332
|
+
- Notification templates with context parameters
|
|
333
|
+
- File deduplication via checksum
|
|
334
|
+
- Attachment cleanup for orphaned files
|
|
335
|
+
- FHIR-compliant data storage
|
|
336
|
+
|
|
337
|
+
### ❌ Not Yet Supported
|
|
338
|
+
|
|
339
|
+
- SMS notifications (Medplum limitation)
|
|
340
|
+
- Push notifications (Medplum limitation)
|
|
341
|
+
- In-app notification UI
|
|
342
|
+
- Real-time notification delivery (requires polling or webhooks)
|
|
343
|
+
|
|
344
|
+
## API Reference
|
|
345
|
+
|
|
346
|
+
### MedplumNotificationBackend
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
class MedplumNotificationBackend<Config extends BaseNotificationTypeConfig>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Main backend for storing notifications as FHIR Communication resources.
|
|
353
|
+
|
|
354
|
+
**Constructor:**
|
|
355
|
+
```typescript
|
|
356
|
+
constructor(
|
|
357
|
+
medplum: MedplumClient,
|
|
358
|
+
options?: {
|
|
359
|
+
emailNotificationSubjectExtensionUrl?: string;
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Key Methods:**
|
|
365
|
+
- `persistNotification(notification)` - Create a new notification
|
|
366
|
+
- `persistOneOffNotification(notification)` - Create a one-off notification
|
|
367
|
+
- `getNotification(id)` - Retrieve a notification by ID
|
|
368
|
+
- `getPendingNotifications(page, pageSize)` - Get notifications ready to send
|
|
369
|
+
- `markAsSent(id)` - Mark notification as successfully sent
|
|
370
|
+
- `markAsFailed(id)` - Mark notification as failed
|
|
371
|
+
- `cancelNotification(id)` - Cancel a scheduled notification
|
|
372
|
+
|
|
373
|
+
### MedplumNotificationAdapter
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
class MedplumNotificationAdapter<
|
|
377
|
+
TemplateRenderer extends BaseEmailTemplateRenderer<Config>,
|
|
378
|
+
Config extends BaseNotificationTypeConfig
|
|
379
|
+
>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Adapter for sending email notifications via Medplum.
|
|
383
|
+
|
|
384
|
+
**Constructor:**
|
|
385
|
+
```typescript
|
|
386
|
+
constructor(
|
|
387
|
+
medplum: MedplumClient,
|
|
388
|
+
templateRenderer: TemplateRenderer
|
|
389
|
+
)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Properties:**
|
|
393
|
+
- `supportsAttachments: boolean` - Returns `true`
|
|
394
|
+
|
|
395
|
+
**Key Methods:**
|
|
396
|
+
- `send(notification, context)` - Send an email notification with attachments
|
|
397
|
+
|
|
398
|
+
### MedplumAttachmentManager
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
class MedplumAttachmentManager extends BaseAttachmentManager
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Manages file attachments using FHIR Binary and Media resources.
|
|
405
|
+
|
|
406
|
+
**Constructor:**
|
|
407
|
+
```typescript
|
|
408
|
+
constructor(medplum: MedplumClient)
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Key Methods:**
|
|
412
|
+
- `uploadFile(file, filename, contentType?)` - Upload a file to Medplum storage
|
|
413
|
+
- `getFile(fileId)` - Retrieve file metadata by Media resource ID
|
|
414
|
+
- `deleteFile(fileId)` - Delete file and its Binary resource
|
|
415
|
+
- `reconstructAttachmentFile(storageMetadata)` - Create AttachmentFile from metadata
|
|
416
|
+
|
|
417
|
+
### MedplumAttachmentFile
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
class MedplumAttachmentFile implements AttachmentFile
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Provides access to files stored in FHIR Binary resources.
|
|
424
|
+
|
|
425
|
+
**Methods:**
|
|
426
|
+
- `read()` - Read entire file into a Buffer
|
|
427
|
+
- `stream()` - Get a ReadableStream for the file
|
|
428
|
+
- `url(expiresIn?)` - Generate URL for file access
|
|
429
|
+
- `delete()` - Delete the file from storage
|
|
430
|
+
|
|
431
|
+
## Best Practices
|
|
432
|
+
|
|
433
|
+
### 1. Use FHIR References Consistently
|
|
434
|
+
|
|
435
|
+
Always use proper FHIR reference format for user IDs:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// ✅ Good
|
|
439
|
+
userId: 'Patient/123'
|
|
440
|
+
userId: 'Practitioner/456'
|
|
441
|
+
|
|
442
|
+
// ❌ Bad
|
|
443
|
+
userId: '123'
|
|
444
|
+
userId: 'user-456'
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 2. Configure Custom Extension URLs
|
|
448
|
+
|
|
449
|
+
Use your own domain for extension URLs in production:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
const backend = new MedplumNotificationBackend(medplum, {
|
|
453
|
+
emailNotificationSubjectExtensionUrl: 'http://your-domain.com/fhir/email-subject',
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### 3. Handle Large Attachments Carefully
|
|
458
|
+
|
|
459
|
+
For large files, consider:
|
|
460
|
+
- Using file size limits
|
|
461
|
+
- Implementing file compression
|
|
462
|
+
- Using streaming for file operations
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// Check file size before upload
|
|
466
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
467
|
+
if (buffer.length > maxSize) {
|
|
468
|
+
throw new Error('File too large');
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 4. Clean Up Orphaned Files
|
|
473
|
+
|
|
474
|
+
Regularly run cleanup to remove orphaned attachment files:
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// Get orphaned files
|
|
478
|
+
const orphaned = await backend.getOrphanedAttachmentFiles();
|
|
479
|
+
|
|
480
|
+
// Delete them
|
|
481
|
+
for (const file of orphaned) {
|
|
482
|
+
await backend.deleteAttachmentFile(file.id);
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 5. Use Pagination
|
|
487
|
+
|
|
488
|
+
Always paginate when fetching large result sets:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// ✅ Good
|
|
492
|
+
const notifications = await notificationService.getPendingNotifications(0, 50);
|
|
493
|
+
|
|
494
|
+
// ❌ Bad - could return thousands of records
|
|
495
|
+
const notifications = await notificationService.getAllPendingNotifications();
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## License
|
|
499
|
+
|
|
500
|
+
MIT
|
|
501
|
+
|
|
502
|
+
## Support
|
|
503
|
+
|
|
504
|
+
For issues and questions:
|
|
505
|
+
- VintaSend: [GitHub Issues](https://github.com/vintasoftware/vintasend-ts/issues)
|
|
506
|
+
- Medplum: [Documentation](https://www.medplum.com/docs)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MedplumNotificationAdapter = exports.MedplumNotificationBackendFactory = exports.MedplumNotificationBackend = exports.MedplumAttachmentFile = exports.MedplumAttachmentManager = void 0;
|
|
4
|
+
var medplum_attachment_manager_1 = require("./medplum-attachment-manager");
|
|
5
|
+
Object.defineProperty(exports, "MedplumAttachmentManager", { enumerable: true, get: function () { return medplum_attachment_manager_1.MedplumAttachmentManager; } });
|
|
6
|
+
Object.defineProperty(exports, "MedplumAttachmentFile", { enumerable: true, get: function () { return medplum_attachment_manager_1.MedplumAttachmentFile; } });
|
|
7
|
+
var medplum_backend_1 = require("./medplum-backend");
|
|
8
|
+
Object.defineProperty(exports, "MedplumNotificationBackend", { enumerable: true, get: function () { return medplum_backend_1.MedplumNotificationBackend; } });
|
|
9
|
+
Object.defineProperty(exports, "MedplumNotificationBackendFactory", { enumerable: true, get: function () { return medplum_backend_1.MedplumNotificationBackendFactory; } });
|
|
10
|
+
var medplum_adapter_1 = require("./medplum-adapter");
|
|
11
|
+
Object.defineProperty(exports, "MedplumNotificationAdapter", { enumerable: true, get: function () { return medplum_adapter_1.MedplumNotificationAdapter; } });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MedplumClient } from '@medplum/core';
|
|
2
|
+
import type { BaseEmailTemplateRenderer } from 'vintasend/dist/services/notification-template-renderers/base-email-template-renderer';
|
|
3
|
+
import { BaseNotificationAdapter } from 'vintasend/dist/services/notification-adapters/base-notification-adapter';
|
|
4
|
+
import type { JsonObject } from 'vintasend/dist/types/json-values';
|
|
5
|
+
import type { AnyDatabaseNotification } from 'vintasend/dist/types/notification';
|
|
6
|
+
import type { BaseNotificationTypeConfig } from 'vintasend/dist/types/notification-type-config';
|
|
7
|
+
import type { StoredAttachment } from 'vintasend/dist/types/attachment';
|
|
8
|
+
export declare class MedplumNotificationAdapter<TemplateRenderer extends BaseEmailTemplateRenderer<Config>, Config extends BaseNotificationTypeConfig> extends BaseNotificationAdapter<TemplateRenderer, Config> {
|
|
9
|
+
private medplum;
|
|
10
|
+
key: string | null;
|
|
11
|
+
constructor(medplum: MedplumClient, templateRenderer: TemplateRenderer);
|
|
12
|
+
get supportsAttachments(): boolean;
|
|
13
|
+
send(notification: AnyDatabaseNotification<Config>, context: JsonObject): Promise<void>;
|
|
14
|
+
protected prepareAttachments(attachments: StoredAttachment[]): Promise<Array<{
|
|
15
|
+
filename: string;
|
|
16
|
+
content: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
}>>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MedplumNotificationAdapter = void 0;
|
|
4
|
+
const base_notification_adapter_1 = require("vintasend/dist/services/notification-adapters/base-notification-adapter");
|
|
5
|
+
class MedplumNotificationAdapter extends base_notification_adapter_1.BaseNotificationAdapter {
|
|
6
|
+
constructor(medplum, templateRenderer) {
|
|
7
|
+
const notificationType = 'EMAIL';
|
|
8
|
+
super(templateRenderer, notificationType, false);
|
|
9
|
+
this.medplum = medplum;
|
|
10
|
+
this.key = 'medplum-email';
|
|
11
|
+
}
|
|
12
|
+
get supportsAttachments() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
async send(notification, context) {
|
|
16
|
+
if (!this.backend) {
|
|
17
|
+
throw new Error('Backend not injected');
|
|
18
|
+
}
|
|
19
|
+
if (!notification.id) {
|
|
20
|
+
throw new Error('Notification ID is required');
|
|
21
|
+
}
|
|
22
|
+
// Use the helper method to get recipient email (handles both regular and one-off notifications)
|
|
23
|
+
const recipientEmail = await this.getRecipientEmail(notification);
|
|
24
|
+
const template = await this.templateRenderer.render(notification, context);
|
|
25
|
+
const emailOptions = {
|
|
26
|
+
to: recipientEmail,
|
|
27
|
+
subject: template.subject,
|
|
28
|
+
text: template.body,
|
|
29
|
+
};
|
|
30
|
+
// Add attachments if present
|
|
31
|
+
// Type assertion needed because npm version may not have attachments field yet
|
|
32
|
+
const notificationWithAttachments = notification;
|
|
33
|
+
if (notificationWithAttachments.attachments && notificationWithAttachments.attachments.length > 0) {
|
|
34
|
+
emailOptions.attachments = await this.prepareAttachments(notificationWithAttachments.attachments);
|
|
35
|
+
}
|
|
36
|
+
await this.medplum.sendEmail(emailOptions);
|
|
37
|
+
}
|
|
38
|
+
async prepareAttachments(attachments) {
|
|
39
|
+
return Promise.all(attachments.map(async (att) => {
|
|
40
|
+
const buffer = await att.file.read();
|
|
41
|
+
return {
|
|
42
|
+
filename: att.filename,
|
|
43
|
+
content: buffer.toString('base64'),
|
|
44
|
+
contentType: att.contentType,
|
|
45
|
+
};
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.MedplumNotificationAdapter = MedplumNotificationAdapter;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { MedplumClient } from '@medplum/core';
|
|
2
|
+
import { BaseAttachmentManager } from 'vintasend/dist/services/attachment-manager/base-attachment-manager';
|
|
3
|
+
import type { AttachmentFileRecord, AttachmentFile, FileAttachment } from 'vintasend/dist/types/attachment';
|
|
4
|
+
/**
|
|
5
|
+
* Medplum AttachmentManager implementation.
|
|
6
|
+
*
|
|
7
|
+
* This implementation uses FHIR resources for file storage:
|
|
8
|
+
* - Binary resources store the actual file data
|
|
9
|
+
* - Media resources store file metadata and link to Binary resources
|
|
10
|
+
*
|
|
11
|
+
* Files are uploaded to Medplum's storage and can be accessed via URLs.
|
|
12
|
+
*/
|
|
13
|
+
export declare class MedplumAttachmentManager extends BaseAttachmentManager {
|
|
14
|
+
private medplum;
|
|
15
|
+
constructor(medplum: MedplumClient);
|
|
16
|
+
/**
|
|
17
|
+
* Upload a file to Medplum storage using Binary and Media resources.
|
|
18
|
+
*
|
|
19
|
+
* @param file - The file data (Buffer, ReadableStream, or file path)
|
|
20
|
+
* @param filename - The filename to use for storage
|
|
21
|
+
* @param contentType - Optional MIME type (auto-detected if not provided)
|
|
22
|
+
* @returns AttachmentFileRecord with metadata about the uploaded file
|
|
23
|
+
*/
|
|
24
|
+
uploadFile(file: FileAttachment, filename: string, contentType?: string): Promise<AttachmentFileRecord>;
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve a file record by its Media resource ID.
|
|
27
|
+
*
|
|
28
|
+
* @param fileId - The Media resource ID
|
|
29
|
+
* @returns The file record or null if not found
|
|
30
|
+
*/
|
|
31
|
+
getFile(fileId: string): Promise<AttachmentFileRecord | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Delete a file from Medplum storage.
|
|
34
|
+
*
|
|
35
|
+
* Deletes both the Media resource (metadata) and Binary resource (file data).
|
|
36
|
+
*
|
|
37
|
+
* @param fileId - The Media resource ID
|
|
38
|
+
*/
|
|
39
|
+
deleteFile(fileId: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Reconstruct an AttachmentFile from storage metadata.
|
|
42
|
+
*
|
|
43
|
+
* @param storageMetadata - Metadata containing either 'url' (Binary URL) or 'binaryId'
|
|
44
|
+
* @returns AttachmentFile instance for accessing the file
|
|
45
|
+
*/
|
|
46
|
+
reconstructAttachmentFile(storageMetadata: Record<string, unknown>): AttachmentFile;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Medplum AttachmentFile implementation.
|
|
50
|
+
*
|
|
51
|
+
* Provides access to files stored in Medplum Binary resources.
|
|
52
|
+
*/
|
|
53
|
+
export declare class MedplumAttachmentFile implements AttachmentFile {
|
|
54
|
+
private medplum;
|
|
55
|
+
private binaryId;
|
|
56
|
+
constructor(medplum: MedplumClient, binaryUrl: string);
|
|
57
|
+
/**
|
|
58
|
+
* Read the entire file into memory as a Buffer.
|
|
59
|
+
*/
|
|
60
|
+
read(): Promise<Buffer>;
|
|
61
|
+
/**
|
|
62
|
+
* Get a readable stream for the file.
|
|
63
|
+
*
|
|
64
|
+
* Note: For Medplum, we read the entire file and convert to stream.
|
|
65
|
+
* For true streaming, you'd need to use Medplum's binary download API.
|
|
66
|
+
*/
|
|
67
|
+
stream(): Promise<ReadableStream>;
|
|
68
|
+
/**
|
|
69
|
+
* Generate a URL for accessing the file.
|
|
70
|
+
*
|
|
71
|
+
* For Medplum, this returns the Binary resource URL.
|
|
72
|
+
* In production, you might want to generate a presigned URL.
|
|
73
|
+
*
|
|
74
|
+
* @param expiresIn - Seconds until the URL expires (not used for Medplum)
|
|
75
|
+
* @returns URL for file access
|
|
76
|
+
*/
|
|
77
|
+
url(expiresIn?: number): Promise<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Delete this file from storage.
|
|
80
|
+
*/
|
|
81
|
+
delete(): Promise<void>;
|
|
82
|
+
}
|