vintasend-medplum 0.4.1 → 0.4.4

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 CHANGED
@@ -11,8 +11,61 @@ This implementation uses FHIR (Fast Healthcare Interoperability Resources) stand
11
11
  - **MedplumNotificationBackend**: Stores notifications as FHIR `Communication` resources
12
12
  - **MedplumNotificationAdapter**: Sends email notifications via Medplum's email API
13
13
  - **MedplumAttachmentManager**: Manages file attachments using FHIR `Binary` and `Media` resources
14
+ - **PugInlineEmailTemplateRenderer**: Renders Pug email templates from pre-compiled JSON (ideal for production)
14
15
  - **MedplumLogger**: Simple console-based logger
15
16
 
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Install the package
21
+ npm install vintasend-medplum @medplum/core
22
+
23
+ # Compile your Pug templates
24
+ npx compile-pug-templates ./templates ./src/compiled-templates.json
25
+ ```
26
+
27
+ ```typescript
28
+ import { MedplumClient } from '@medplum/core';
29
+ import {
30
+ MedplumNotificationBackend,
31
+ MedplumNotificationAdapter,
32
+ PugInlineEmailTemplateRenderer,
33
+ MedplumLogger
34
+ } from 'vintasend-medplum';
35
+ import { NotificationService } from 'vintasend';
36
+ import compiledTemplates from './compiled-templates.json';
37
+
38
+ // Initialize Medplum client
39
+ const medplum = new MedplumClient({
40
+ baseUrl: 'https://api.medplum.com',
41
+ clientId: 'your-client-id',
42
+ clientSecret: 'your-client-secret',
43
+ });
44
+
45
+ // Create services
46
+ const renderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
47
+ const adapter = new MedplumNotificationAdapter(medplum, renderer);
48
+ const backend = new MedplumNotificationBackend(medplum);
49
+ const logger = new MedplumLogger();
50
+
51
+ // Initialize notification service
52
+ const notificationService = new NotificationService(backend, [adapter], logger);
53
+
54
+ // Send a notification
55
+ await notificationService.createNotification({
56
+ userId: 'Patient/123',
57
+ notificationType: 'EMAIL',
58
+ contextName: 'welcome',
59
+ contextParameters: { firstName: 'John' },
60
+ title: 'Welcome!',
61
+ bodyTemplate: 'welcome.pug',
62
+ subjectTemplate: 'subjects/welcome.pug',
63
+ sendAfter: new Date(),
64
+ });
65
+
66
+ await notificationService.processPendingNotifications();
67
+ ```
68
+
16
69
  ## How It Works
17
70
 
18
71
  ### FHIR Resource Mapping
@@ -94,6 +147,76 @@ npm install vintasend-medplum @medplum/core
94
147
 
95
148
  ## Setup
96
149
 
150
+ ### Template Compilation
151
+
152
+ VintaSend Medplum uses pre-compiled Pug email templates that are embedded in your application as JSON. This approach ensures templates are bundled with your code and don't require file system access at runtime.
153
+
154
+ **Step 1: Organize Your Templates**
155
+
156
+ Create a directory structure for your templates:
157
+ ```
158
+ templates/
159
+ welcome.pug
160
+ password-reset.pug
161
+ notifications/
162
+ order-confirmation.pug
163
+ shipment-update.pug
164
+ ```
165
+
166
+ **Step 2: Compile Templates**
167
+
168
+ Run the compilation script using npx:
169
+
170
+ ```bash
171
+ npx compile-pug-templates [input-directory] [output-file]
172
+ ```
173
+
174
+ Both arguments are optional:
175
+ - `input-directory`: Directory containing .pug templates (default: `./templates`)
176
+ - `output-file`: Output JSON file path (default: `compiled-templates.json`)
177
+
178
+ Examples:
179
+ ```bash
180
+ # Use default values (./templates → compiled-templates.json)
181
+ npx compile-pug-templates
182
+
183
+ # Specify only input directory (output to compiled-templates.json)
184
+ npx compile-pug-templates ./email-templates
185
+
186
+ # Specify both arguments
187
+ npx compile-pug-templates ./templates ./src/compiled-templates.json
188
+ ```
189
+
190
+ Or add it to your package.json scripts:
191
+ ```json
192
+ {
193
+ "scripts": {
194
+ "compile-templates": "compile-pug-templates ./templates ./src/compiled-templates.json"
195
+ }
196
+ }
197
+ ```
198
+
199
+ This generates a JSON file where keys are relative paths and values are template contents:
200
+ ```json
201
+ {
202
+ "welcome.pug": "doctype html\nhtml\n body\n h1 Welcome {{firstName}}!",
203
+ "notifications/order-confirmation.pug": "doctype html\n..."
204
+ }
205
+ ```
206
+
207
+ **Step 3: Import and Use Compiled Templates**
208
+
209
+ ```typescript
210
+ import { PugInlineEmailTemplateRenderer } from 'vintasend-medplum';
211
+ import compiledTemplates from './compiled-templates.json';
212
+
213
+ // Create the template renderer with compiled templates
214
+ const templateRenderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
215
+
216
+ // Use with notification adapter
217
+ const adapter = new MedplumNotificationAdapter(medplum, templateRenderer);
218
+ ```
219
+
97
220
  ### Basic Configuration
98
221
 
99
222
  ```typescript
@@ -151,6 +274,56 @@ const backend = new MedplumNotificationBackend(medplum, {
151
274
 
152
275
  ## Usage Examples
153
276
 
277
+ ### Configuring Templates
278
+
279
+ When creating notifications, reference your templates using the same paths used during compilation:
280
+
281
+ ```typescript
282
+ // If you compiled templates/welcome.pug
283
+ await notificationService.createNotification({
284
+ userId: 'Patient/123',
285
+ notificationType: 'EMAIL',
286
+ contextName: 'user-welcome',
287
+ contextParameters: {
288
+ firstName: 'John',
289
+ lastName: 'Doe',
290
+ },
291
+ title: 'Welcome to our platform!',
292
+ bodyTemplate: 'welcome.pug', // Path from compiled templates
293
+ subjectTemplate: 'subjects/welcome.pug', // Can be in subdirectories
294
+ sendAfter: new Date(),
295
+ });
296
+
297
+ // If you compiled templates/notifications/order-confirmation.pug
298
+ await notificationService.createNotification({
299
+ userId: 'Patient/456',
300
+ notificationType: 'EMAIL',
301
+ contextName: 'order-confirmation',
302
+ contextParameters: {
303
+ orderNumber: '12345',
304
+ totalAmount: '$99.99',
305
+ },
306
+ title: 'Order Confirmation',
307
+ bodyTemplate: 'notifications/order-confirmation.pug',
308
+ subjectTemplate: 'notifications/subjects/order-confirmation.pug',
309
+ sendAfter: new Date(),
310
+ });
311
+ ```
312
+
313
+ **Template Example (welcome.pug):**
314
+ ```pug
315
+ doctype html
316
+ html
317
+ head
318
+ title Welcome
319
+ body
320
+ h1 Welcome #{firstName} #{lastName}!
321
+ p Thank you for joining our platform.
322
+ p
323
+ | If you have any questions, feel free to
324
+ a(href="mailto:support@example.com") contact us
325
+ ```
326
+
154
327
  ### Sending a Simple Notification
155
328
 
156
329
  ```typescript
@@ -395,6 +568,34 @@ constructor(
395
568
  **Key Methods:**
396
569
  - `send(notification, context)` - Send an email notification with attachments
397
570
 
571
+ ### PugInlineEmailTemplateRenderer
572
+
573
+ ```typescript
574
+ class PugInlineEmailTemplateRenderer<Config extends BaseNotificationTypeConfig>
575
+ implements BaseEmailTemplateRenderer<Config>
576
+ ```
577
+
578
+ Template renderer that compiles Pug templates from pre-compiled JSON strings instead of reading from file paths. This is ideal for production deployments where templates are embedded in the application.
579
+
580
+ **Constructor:**
581
+ ```typescript
582
+ constructor(generatedTemplates: Record<string, string>)
583
+ ```
584
+
585
+ **Parameters:**
586
+ - `generatedTemplates` - Object mapping template paths to template content strings (generated by `compile-pug-templates` script)
587
+
588
+ **Key Methods:**
589
+ - `render(notification, context)` - Compile and render both subject and body templates using the notification's template paths
590
+
591
+ **Example:**
592
+ ```typescript
593
+ import compiledTemplates from './compiled-templates.json';
594
+
595
+ const renderer = new PugInlineEmailTemplateRenderer(compiledTemplates);
596
+ const adapter = new MedplumNotificationAdapter(medplum, renderer);
597
+ ```
598
+
398
599
  ### MedplumAttachmentManager
399
600
 
400
601
  ```typescript
@@ -430,7 +631,70 @@ Provides access to files stored in FHIR Binary resources.
430
631
 
431
632
  ## Best Practices
432
633
 
433
- ### 1. Use FHIR References Consistently
634
+ ### 1. Template Organization
635
+
636
+ Organize your templates in a clear directory structure:
637
+
638
+ ```
639
+ templates/
640
+ subjects/ # Email subject templates
641
+ welcome.pug
642
+ order-confirmation.pug
643
+ bodies/ # Or organize by feature
644
+ welcome.pug
645
+ order-confirmation.pug
646
+ notifications/ # Group related templates
647
+ orders/
648
+ confirmation.pug
649
+ shipped.pug
650
+ users/
651
+ welcome.pug
652
+ password-reset.pug
653
+ ```
654
+
655
+ ### 2. Template Naming Conventions
656
+
657
+ Use consistent, descriptive names:
658
+ ```typescript
659
+ // ✅ Good - clear and descriptive
660
+ bodyTemplate: 'notifications/orders/confirmation.pug'
661
+ subjectTemplate: 'subjects/order-confirmation.pug'
662
+
663
+ // ❌ Bad - unclear purpose
664
+ bodyTemplate: 'template1.pug'
665
+ subjectTemplate: 'subj.pug'
666
+ ```
667
+
668
+ ### 3. Template Variables
669
+
670
+ Document the expected context variables in each template:
671
+
672
+ ```pug
673
+ //- templates/welcome.pug
674
+ //- Expected variables: firstName, lastName, loginUrl
675
+ doctype html
676
+ html
677
+ body
678
+ h1 Welcome #{firstName} #{lastName}!
679
+ a(href=loginUrl) Login to your account
680
+ ```
681
+
682
+ ### 4. Compilation in Build Process
683
+
684
+ Add template compilation to your build pipeline:
685
+
686
+ ```json
687
+ {
688
+ "scripts": {
689
+ "compile-templates": "compile-pug-templates ./templates ./src/compiled-templates.json",
690
+ "build": "npm run compile-templates && tsc"
691
+ }
692
+ }
693
+ ```
694
+
695
+ This ensures templates are always compiled before building your application.
696
+
697
+ ### 5. Use FHIR References Consistently
434
698
 
435
699
  Always use proper FHIR reference format for user IDs:
436
700
 
@@ -444,7 +708,7 @@ userId: '123'
444
708
  userId: 'user-456'
445
709
  ```
446
710
 
447
- ### 2. Configure Custom Extension URLs
711
+ ### 6. Configure Custom Extension URLs
448
712
 
449
713
  Use your own domain for extension URLs in production:
450
714
 
@@ -454,7 +718,7 @@ const backend = new MedplumNotificationBackend(medplum, {
454
718
  });
455
719
  ```
456
720
 
457
- ### 3. Handle Large Attachments Carefully
721
+ ### 7. Handle Large Attachments Carefully
458
722
 
459
723
  For large files, consider:
460
724
  - Using file size limits
@@ -469,7 +733,7 @@ if (buffer.length > maxSize) {
469
733
  }
470
734
  ```
471
735
 
472
- ### 4. Clean Up Orphaned Files
736
+ ### 8. Clean Up Orphaned Files
473
737
 
474
738
  Regularly run cleanup to remove orphaned attachment files:
475
739
 
@@ -483,7 +747,7 @@ for (const file of orphaned) {
483
747
  }
484
748
  ```
485
749
 
486
- ### 5. Use Pagination
750
+ ### 9. Use Pagination
487
751
 
488
752
  Always paginate when fetching large result sets:
489
753
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { MedplumAttachmentManager, MedplumAttachmentFile } from './medplum-attachment-manager';
2
2
  export { MedplumNotificationBackend, MedplumNotificationBackendFactory } from './medplum-backend';
3
- export { MedplumNotificationAdapter } from './medplum-adapter';
3
+ export { MedplumNotificationAdapter, MedplumNotificationAdapterFactory } from './medplum-adapter';
4
+ export { PugInlineEmailTemplateRenderer, PugInlineEmailTemplateRendererFactory } from './pug-inline-email-template-renderer';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,0BAA0B,EAAE,iCAAiC,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,0BAA0B,EAAE,iCAAiC,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,8BAA8B,EAAE,qCAAqC,EAAE,MAAM,sCAAsC,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { MedplumAttachmentManager, MedplumAttachmentFile } from './medplum-attachment-manager';
2
2
  export { MedplumNotificationBackend, MedplumNotificationBackendFactory } from './medplum-backend';
3
- export { MedplumNotificationAdapter } from './medplum-adapter';
3
+ export { MedplumNotificationAdapter, MedplumNotificationAdapterFactory } from './medplum-adapter';
4
+ export { PugInlineEmailTemplateRenderer, PugInlineEmailTemplateRendererFactory } from './pug-inline-email-template-renderer';
@@ -8,7 +8,7 @@ import type { StoredAttachment } from 'vintasend/dist/types/attachment';
8
8
  export declare class MedplumNotificationAdapter<TemplateRenderer extends BaseEmailTemplateRenderer<Config>, Config extends BaseNotificationTypeConfig> extends BaseNotificationAdapter<TemplateRenderer, Config> {
9
9
  private medplum;
10
10
  key: string | null;
11
- constructor(medplum: MedplumClient, templateRenderer: TemplateRenderer);
11
+ constructor(medplum: MedplumClient, templateRenderer: TemplateRenderer, enqueueNotifications: boolean);
12
12
  get supportsAttachments(): boolean;
13
13
  send(notification: AnyDatabaseNotification<Config>, context: JsonObject): Promise<void>;
14
14
  protected prepareAttachments(attachments: StoredAttachment[]): Promise<Array<{
@@ -17,3 +17,7 @@ export declare class MedplumNotificationAdapter<TemplateRenderer extends BaseEma
17
17
  contentType: string;
18
18
  }>>;
19
19
  }
20
+ export declare class MedplumNotificationAdapterFactory<Config extends BaseNotificationTypeConfig> {
21
+ create<TemplateRenderer extends BaseEmailTemplateRenderer<Config>>(medplum: MedplumClient, templateRenderer: TemplateRenderer, enqueueNotifications: boolean): MedplumNotificationAdapter<TemplateRenderer, Config>;
22
+ }
23
+ //# sourceMappingURL=medplum-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"medplum-adapter.d.ts","sourceRoot":"","sources":["../src/medplum-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,sFAAsF,CAAC;AACtI,OAAO,EAAE,uBAAuB,EAAE,MAAM,yEAAyE,CAAC;AAClH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,qBAAa,0BAA0B,CACrC,gBAAgB,SAAS,yBAAyB,CAAC,MAAM,CAAC,EAC1D,MAAM,SAAS,0BAA0B,CACzC,SAAQ,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAIvD,OAAO,CAAC,OAAO;IAHV,GAAG,EAAE,MAAM,GAAG,IAAI,CAAmB;gBAGlC,OAAO,EAAE,aAAa,EAC9B,gBAAgB,EAAE,gBAAgB,EAClC,oBAAoB,EAAE,OAAO;IAM/B,IAAI,mBAAmB,IAAI,OAAO,CAEjC;IAEK,IAAI,CAAC,YAAY,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;cA0C7E,kBAAkB,CAChC,WAAW,EAAE,gBAAgB,EAAE,GAC9B,OAAO,CAAC,KAAK,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;CAYJ;AAID,qBAAa,iCAAiC,CAAC,MAAM,SAAS,0BAA0B;IACtF,MAAM,CAAC,gBAAgB,SAAS,yBAAyB,CAAC,MAAM,CAAC,EAC/D,OAAO,EAAE,aAAa,EACtB,gBAAgB,EAAE,gBAAgB,EAClC,oBAAoB,EAAE,OAAO;CAQhC"}
@@ -1,8 +1,8 @@
1
1
  import { BaseNotificationAdapter } from 'vintasend/dist/services/notification-adapters/base-notification-adapter';
2
2
  export class MedplumNotificationAdapter extends BaseNotificationAdapter {
3
- constructor(medplum, templateRenderer) {
3
+ constructor(medplum, templateRenderer, enqueueNotifications) {
4
4
  const notificationType = 'EMAIL';
5
- super(templateRenderer, notificationType, false);
5
+ super(templateRenderer, notificationType, enqueueNotifications);
6
6
  this.medplum = medplum;
7
7
  this.key = 'medplum-email';
8
8
  }
@@ -43,3 +43,8 @@ export class MedplumNotificationAdapter extends BaseNotificationAdapter {
43
43
  }));
44
44
  }
45
45
  }
46
+ export class MedplumNotificationAdapterFactory {
47
+ create(medplum, templateRenderer, enqueueNotifications) {
48
+ return new MedplumNotificationAdapter(medplum, templateRenderer, enqueueNotifications);
49
+ }
50
+ }
@@ -80,3 +80,4 @@ export declare class MedplumAttachmentFile implements AttachmentFile {
80
80
  */
81
81
  delete(): Promise<void>;
82
82
  }
83
+ //# sourceMappingURL=medplum-attachment-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"medplum-attachment-manager.d.ts","sourceRoot":"","sources":["../src/medplum-attachment-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oEAAoE,CAAC;AAC3G,OAAO,KAAK,EACV,oBAAoB,EACpB,cAAc,EACd,cAAc,EACf,MAAM,iCAAiC,CAAC;AAEzC;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,qBAAqB;IACrD,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,aAAa;IAI1C;;;;;;;OAOG;IACG,UAAU,CACd,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,oBAAoB,CAAC;IA6DhC;;;;;OAKG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA0BnE;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB/C;;;;;OAKG;IACH,yBAAyB,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc;CAgBpF;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAIxD,OAAO,CAAC,OAAO;IAHjB,OAAO,CAAC,QAAQ,CAAS;gBAGf,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,MAAM;IAUnB;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAQ7B;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC;IAUvC;;;;;;;;OAQG;IACG,GAAG,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAO5C;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAG9B"}
@@ -22,7 +22,6 @@ export class MedplumAttachmentManager extends BaseAttachmentManager {
22
22
  * @returns AttachmentFileRecord with metadata about the uploaded file
23
23
  */
24
24
  async uploadFile(file, filename, contentType) {
25
- var _a, _b;
26
25
  // Convert file to Buffer
27
26
  const buffer = await this.fileToBuffer(file);
28
27
  // Calculate checksum for deduplication
@@ -71,8 +70,8 @@ export class MedplumAttachmentManager extends BaseAttachmentManager {
71
70
  url: binaryUrl,
72
71
  creation: createdMedia.content.creation,
73
72
  },
74
- createdAt: ((_a = createdMedia.meta) === null || _a === void 0 ? void 0 : _a.lastUpdated) ? new Date(createdMedia.meta.lastUpdated) : new Date(),
75
- updatedAt: ((_b = createdMedia.meta) === null || _b === void 0 ? void 0 : _b.lastUpdated) ? new Date(createdMedia.meta.lastUpdated) : new Date(),
73
+ createdAt: createdMedia.meta?.lastUpdated ? new Date(createdMedia.meta.lastUpdated) : new Date(),
74
+ updatedAt: createdMedia.meta?.lastUpdated ? new Date(createdMedia.meta.lastUpdated) : new Date(),
76
75
  };
77
76
  }
78
77
  /**
@@ -82,7 +81,6 @@ export class MedplumAttachmentManager extends BaseAttachmentManager {
82
81
  * @returns The file record or null if not found
83
82
  */
84
83
  async getFile(fileId) {
85
- var _a, _b, _c, _d;
86
84
  try {
87
85
  const media = await this.medplum.readResource('Media', fileId);
88
86
  if (!media.content || !media.id) {
@@ -93,16 +91,16 @@ export class MedplumAttachmentManager extends BaseAttachmentManager {
93
91
  filename: media.content.title || 'untitled',
94
92
  contentType: media.content.contentType || 'application/octet-stream',
95
93
  size: media.content.size || 0,
96
- checksum: ((_b = (_a = media.identifier) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value) || '',
94
+ checksum: media.identifier?.[0]?.value || '',
97
95
  storageMetadata: {
98
96
  url: media.content.url,
99
97
  creation: media.content.creation,
100
98
  },
101
- createdAt: ((_c = media.meta) === null || _c === void 0 ? void 0 : _c.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
102
- updatedAt: ((_d = media.meta) === null || _d === void 0 ? void 0 : _d.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
99
+ createdAt: media.meta?.lastUpdated ? new Date(media.meta.lastUpdated) : new Date(),
100
+ updatedAt: media.meta?.lastUpdated ? new Date(media.meta.lastUpdated) : new Date(),
103
101
  };
104
102
  }
105
- catch (_e) {
103
+ catch {
106
104
  return null;
107
105
  }
108
106
  }
@@ -114,11 +112,10 @@ export class MedplumAttachmentManager extends BaseAttachmentManager {
114
112
  * @param fileId - The Media resource ID
115
113
  */
116
114
  async deleteFile(fileId) {
117
- var _a;
118
115
  // Get Media resource to find the Binary URL
119
116
  const media = await this.medplum.readResource('Media', fileId);
120
117
  // Extract Binary ID from URL
121
- const binaryUrl = (_a = media.content) === null || _a === void 0 ? void 0 : _a.url;
118
+ const binaryUrl = media.content?.url;
122
119
  if (binaryUrl) {
123
120
  const match = binaryUrl.match(/Binary\/([^/]+)/);
124
121
  if (match) {
@@ -84,3 +84,4 @@ export declare class MedplumNotificationBackendFactory<Config extends BaseNotifi
84
84
  create(medplum: MedplumClient): MedplumNotificationBackend<Config>;
85
85
  }
86
86
  export {};
87
+ //# sourceMappingURL=medplum-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"medplum-backend.d.ts","sourceRoot":"","sources":["../src/medplum-backend.ts"],"names":[],"mappings":"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"}
@@ -21,27 +21,26 @@ export class MedplumNotificationBackend {
21
21
  return this.attachmentManager;
22
22
  }
23
23
  mapToDatabaseNotification(communication) {
24
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
25
24
  const notificationId = communication.id;
26
- const referenceString = (_b = (_a = communication.recipient) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.reference;
25
+ const referenceString = communication.recipient?.[0]?.reference;
27
26
  // Extract subjectTemplate from payload extension
28
- const subjectTemplateExtension = (_e = (_d = (_c = communication.payload) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.extension) === null || _e === void 0 ? void 0 : _e.find((ext) => ext.url === this.options.emailNotificationSubjectExtensionUrl);
29
- const subjectTemplate = (subjectTemplateExtension === null || subjectTemplateExtension === void 0 ? void 0 : subjectTemplateExtension.valueString) || null;
27
+ const subjectTemplateExtension = communication.payload?.[0]?.extension?.find((ext) => ext.url === this.options.emailNotificationSubjectExtensionUrl);
28
+ const subjectTemplate = subjectTemplateExtension?.valueString || null;
30
29
  // Extract one-off notification fields from extensions
31
- const emailOrPhoneExtension = (_f = communication.extension) === null || _f === void 0 ? void 0 : _f.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/emailOrPhone');
32
- const firstNameExtension = (_g = communication.extension) === null || _g === void 0 ? void 0 : _g.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/firstName');
33
- const lastNameExtension = (_h = communication.extension) === null || _h === void 0 ? void 0 : _h.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/lastName');
34
- const emailOrPhone = emailOrPhoneExtension === null || emailOrPhoneExtension === void 0 ? void 0 : emailOrPhoneExtension.valueString;
35
- const firstName = firstNameExtension === null || firstNameExtension === void 0 ? void 0 : firstNameExtension.valueString;
36
- const lastName = lastNameExtension === null || lastNameExtension === void 0 ? void 0 : lastNameExtension.valueString;
30
+ const emailOrPhoneExtension = communication.extension?.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/emailOrPhone');
31
+ const firstNameExtension = communication.extension?.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/firstName');
32
+ const lastNameExtension = communication.extension?.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/lastName');
33
+ const emailOrPhone = emailOrPhoneExtension?.valueString;
34
+ const firstName = firstNameExtension?.valueString;
35
+ const lastName = lastNameExtension?.valueString;
37
36
  const baseNotification = {
38
37
  id: notificationId,
39
- notificationType: (_l = (_k = (_j = communication.meta) === null || _j === void 0 ? void 0 : _j.tag) === null || _k === void 0 ? void 0 : _k[2]) === null || _l === void 0 ? void 0 : _l.code,
40
- title: ((_m = communication.topic) === null || _m === void 0 ? void 0 : _m.text) || null,
41
- contextName: (_q = (_p = (_o = communication.meta) === null || _o === void 0 ? void 0 : _o.tag) === null || _p === void 0 ? void 0 : _p[1]) === null || _q === void 0 ? void 0 : _q.code,
42
- contextParameters: JSON.parse(((_s = (_r = communication.note) === null || _r === void 0 ? void 0 : _r[0]) === null || _s === void 0 ? void 0 : _s.text) || '{}'),
38
+ notificationType: communication.meta?.tag?.[2]?.code,
39
+ title: communication.topic?.text || null,
40
+ contextName: communication.meta?.tag?.[1]?.code,
41
+ contextParameters: JSON.parse(communication.note?.[0]?.text || '{}'),
43
42
  sendAfter: communication.sent ? new Date(communication.sent) : null,
44
- bodyTemplate: ((_u = (_t = communication.payload) === null || _t === void 0 ? void 0 : _t[0]) === null || _u === void 0 ? void 0 : _u.contentString) || '',
43
+ bodyTemplate: communication.payload?.[0]?.contentString || '',
45
44
  subjectTemplate: subjectTemplate,
46
45
  extraParams: {},
47
46
  status: communication.status === 'completed' ? 'SENT' : communication.status === 'stopped' ? 'FAILED' : 'PENDING_SEND',
@@ -49,8 +48,8 @@ export class MedplumNotificationBackend {
49
48
  adapterUsed: null,
50
49
  sentAt: communication.status === 'completed' ? new Date(communication.sent || new Date()) : null,
51
50
  readAt: null,
52
- createdAt: new Date(((_v = communication.meta) === null || _v === void 0 ? void 0 : _v.lastUpdated) || new Date()),
53
- updatedAt: new Date(((_w = communication.meta) === null || _w === void 0 ? void 0 : _w.lastUpdated) || new Date()),
51
+ createdAt: new Date(communication.meta?.lastUpdated || new Date()),
52
+ updatedAt: new Date(communication.meta?.lastUpdated || new Date()),
54
53
  };
55
54
  // Check if this is a one-off notification
56
55
  if (emailOrPhone && firstName && lastName) {
@@ -132,7 +131,6 @@ export class MedplumNotificationBackend {
132
131
  .filter((n) => 'userId' in n);
133
132
  }
134
133
  async persistNotification(notification) {
135
- var _a;
136
134
  // Build base payload with body template
137
135
  const payload = notification.bodyTemplate
138
136
  ? [
@@ -154,7 +152,7 @@ export class MedplumNotificationBackend {
154
152
  const communication = {
155
153
  resourceType: 'Communication',
156
154
  status: 'in-progress',
157
- sent: (_a = notification.sendAfter) === null || _a === void 0 ? void 0 : _a.toISOString(),
155
+ sent: notification.sendAfter?.toISOString(),
158
156
  topic: { text: notification.title || undefined },
159
157
  payload,
160
158
  recipient: [{ reference: notification.userId }],
@@ -199,13 +197,12 @@ export class MedplumNotificationBackend {
199
197
  return communications.map((comm) => this.mapToDatabaseNotification(comm));
200
198
  }
201
199
  async bulkPersistNotifications(notifications) {
202
- var _a;
203
200
  const ids = [];
204
201
  for (const notification of notifications) {
205
202
  const communication = {
206
203
  resourceType: 'Communication',
207
204
  status: 'in-progress',
208
- sent: (_a = notification.sendAfter) === null || _a === void 0 ? void 0 : _a.toISOString(),
205
+ sent: notification.sendAfter?.toISOString(),
209
206
  topic: { text: notification.title || undefined },
210
207
  payload: notification.bodyTemplate
211
208
  ? [
@@ -234,7 +231,7 @@ export class MedplumNotificationBackend {
234
231
  }
235
232
  async markAsSent(notificationId, checkIsPending = true) {
236
233
  const notification = await this.getNotification(notificationId, false);
237
- if (checkIsPending && (notification === null || notification === void 0 ? void 0 : notification.status) !== 'PENDING_SEND') {
234
+ if (checkIsPending && notification?.status !== 'PENDING_SEND') {
238
235
  throw new Error('Notification is not pending');
239
236
  }
240
237
  // Use internal update that returns AnyDatabaseNotification
@@ -252,7 +249,7 @@ export class MedplumNotificationBackend {
252
249
  }
253
250
  async markAsFailed(notificationId, checkIsPending = true) {
254
251
  const notification = await this.getNotification(notificationId, false);
255
- if (checkIsPending && (notification === null || notification === void 0 ? void 0 : notification.status) !== 'PENDING_SEND') {
252
+ if (checkIsPending && notification?.status !== 'PENDING_SEND') {
256
253
  throw new Error('Notification is not pending');
257
254
  }
258
255
  // Use internal update that returns AnyDatabaseNotification
@@ -270,7 +267,7 @@ export class MedplumNotificationBackend {
270
267
  }
271
268
  async markAsRead(notificationId, checkIsSent = true) {
272
269
  const notification = await this.getNotification(notificationId, false);
273
- if (checkIsSent && (notification === null || notification === void 0 ? void 0 : notification.status) !== 'SENT') {
270
+ if (checkIsSent && notification?.status !== 'SENT') {
274
271
  throw new Error('Notification is not sent');
275
272
  }
276
273
  // Ensure it's a regular notification (not one-off)
@@ -289,7 +286,7 @@ export class MedplumNotificationBackend {
289
286
  const communication = await this.medplum.readResource('Communication', notificationId);
290
287
  return this.mapToDatabaseNotification(communication);
291
288
  }
292
- catch (_a) {
289
+ catch {
293
290
  return null;
294
291
  }
295
292
  }
@@ -316,10 +313,9 @@ export class MedplumNotificationBackend {
316
313
  .filter((notif) => 'userId' in notif && !notif.readAt);
317
314
  }
318
315
  async getUserEmailFromNotification(notificationId) {
319
- var _a, _b, _c, _d;
320
316
  try {
321
317
  const communication = await this.medplum.readResource('Communication', notificationId);
322
- const practitionerRef = (_b = (_a = communication.recipient) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.reference;
318
+ const practitionerRef = communication.recipient?.[0]?.reference;
323
319
  if (!practitionerRef) {
324
320
  return undefined;
325
321
  }
@@ -330,7 +326,7 @@ export class MedplumNotificationBackend {
330
326
  return undefined;
331
327
  }
332
328
  const resource = await this.medplum.readResource(resourceType, id);
333
- return (_d = (_c = resource.telecom) === null || _c === void 0 ? void 0 : _c.find((t) => t.system === 'email')) === null || _d === void 0 ? void 0 : _d.value;
329
+ return resource.telecom?.find((t) => t.system === 'email')?.value;
334
330
  }
335
331
  catch (error) {
336
332
  // eslint-disable-next-line no-console
@@ -348,7 +344,6 @@ export class MedplumNotificationBackend {
348
344
  }
349
345
  /* One-off notification methods */
350
346
  async persistOneOffNotification(notification) {
351
- var _a;
352
347
  // Build base payload with body template
353
348
  const payload = notification.bodyTemplate
354
349
  ? [
@@ -370,7 +365,7 @@ export class MedplumNotificationBackend {
370
365
  const communication = {
371
366
  resourceType: 'Communication',
372
367
  status: 'in-progress',
373
- sent: (_a = notification.sendAfter) === null || _a === void 0 ? void 0 : _a.toISOString(),
368
+ sent: notification.sendAfter?.toISOString(),
374
369
  topic: { text: notification.title || undefined },
375
370
  payload,
376
371
  note: [{ text: JSON.stringify(notification.contextParameters) }],
@@ -471,7 +466,6 @@ export class MedplumNotificationBackend {
471
466
  * Helper: Convert FHIR Media resource to AttachmentFileRecord
472
467
  */
473
468
  mediaToAttachmentFileRecord(media) {
474
- var _a, _b, _c, _d;
475
469
  const content = media.content;
476
470
  if (!content || !media.id) {
477
471
  throw new Error('Invalid Media resource: missing content or id');
@@ -481,21 +475,20 @@ export class MedplumNotificationBackend {
481
475
  filename: content.title || 'untitled',
482
476
  contentType: content.contentType || 'application/octet-stream',
483
477
  size: content.size || 0,
484
- checksum: ((_b = (_a = media.identifier) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value) || '',
478
+ checksum: media.identifier?.[0]?.value || '',
485
479
  storageMetadata: {
486
480
  url: content.url,
487
481
  creation: content.creation,
488
482
  },
489
- createdAt: ((_c = media.meta) === null || _c === void 0 ? void 0 : _c.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
490
- updatedAt: ((_d = media.meta) === null || _d === void 0 ? void 0 : _d.lastUpdated) ? new Date(media.meta.lastUpdated) : new Date(),
483
+ createdAt: media.meta?.lastUpdated ? new Date(media.meta.lastUpdated) : new Date(),
484
+ updatedAt: media.meta?.lastUpdated ? new Date(media.meta.lastUpdated) : new Date(),
491
485
  };
492
486
  }
493
487
  /**
494
488
  * Helper: Create FHIR Attachment from file record
495
489
  */
496
490
  async createFhirAttachment(fileRecord) {
497
- var _a;
498
- const url = (_a = fileRecord.storageMetadata) === null || _a === void 0 ? void 0 : _a.url;
491
+ const url = fileRecord.storageMetadata?.url;
499
492
  return {
500
493
  contentType: fileRecord.contentType,
501
494
  url: url,
@@ -516,7 +509,7 @@ export class MedplumNotificationBackend {
516
509
  const media = await this.medplum.readResource('Media', fileId);
517
510
  return this.mediaToAttachmentFileRecord(media);
518
511
  }
519
- catch (_a) {
512
+ catch {
520
513
  return null;
521
514
  }
522
515
  }
@@ -528,7 +521,7 @@ export class MedplumNotificationBackend {
528
521
  }
529
522
  return this.mediaToAttachmentFileRecord(results[0]);
530
523
  }
531
- catch (_a) {
524
+ catch {
532
525
  return null;
533
526
  }
534
527
  }
@@ -536,7 +529,6 @@ export class MedplumNotificationBackend {
536
529
  * Find multiple attachment files by their checksums in a single query
537
530
  */
538
531
  async findAttachmentFilesByChecksums(checksums) {
539
- var _a, _b;
540
532
  if (checksums.length === 0) {
541
533
  return new Map();
542
534
  }
@@ -547,14 +539,14 @@ export class MedplumNotificationBackend {
547
539
  // Map results by checksum
548
540
  const filesByChecksum = new Map();
549
541
  for (const media of results) {
550
- const checksum = (_b = (_a = media.identifier) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value;
542
+ const checksum = media.identifier?.[0]?.value;
551
543
  if (checksum) {
552
544
  filesByChecksum.set(checksum, this.mediaToAttachmentFileRecord(media));
553
545
  }
554
546
  }
555
547
  return filesByChecksum;
556
548
  }
557
- catch (_c) {
549
+ catch {
558
550
  return new Map();
559
551
  }
560
552
  }
@@ -648,7 +640,6 @@ export class MedplumNotificationBackend {
648
640
  return payload;
649
641
  }
650
642
  async deleteAttachmentFile(fileId) {
651
- var _a;
652
643
  // Check if file exists first
653
644
  const fileRecord = await this.getAttachmentFile(fileId);
654
645
  if (!fileRecord) {
@@ -659,7 +650,7 @@ export class MedplumNotificationBackend {
659
650
  const communications = await this.medplum.searchResources('Communication', {
660
651
  _tag: 'notification',
661
652
  });
662
- const isReferenced = communications.some((comm) => { var _a; return (_a = comm.payload) === null || _a === void 0 ? void 0 : _a.some((p) => { var _a, _b; return (_b = (_a = p.contentAttachment) === null || _a === void 0 ? void 0 : _a.url) === null || _b === void 0 ? void 0 : _b.includes(fileId); }); });
653
+ const isReferenced = communications.some((comm) => comm.payload?.some((p) => p.contentAttachment?.url?.includes(fileId)));
663
654
  if (isReferenced) {
664
655
  throw new Error('Cannot delete attachment file: still referenced by notifications');
665
656
  }
@@ -668,12 +659,12 @@ export class MedplumNotificationBackend {
668
659
  await this.attachmentManager.deleteFile(fileId);
669
660
  }
670
661
  // Delete Binary resource if it exists
671
- if ((_a = fileRecord.storageMetadata) === null || _a === void 0 ? void 0 : _a.url) {
662
+ if (fileRecord.storageMetadata?.url) {
672
663
  const binaryId = fileRecord.storageMetadata.url.replace('Binary/', '');
673
664
  try {
674
665
  await this.medplum.deleteResource('Binary', binaryId);
675
666
  }
676
- catch (_b) {
667
+ catch {
677
668
  // Binary may not exist or already deleted
678
669
  }
679
670
  }
@@ -681,7 +672,6 @@ export class MedplumNotificationBackend {
681
672
  await this.medplum.deleteResource('Media', fileId);
682
673
  }
683
674
  async getOrphanedAttachmentFiles() {
684
- var _a;
685
675
  // Get all Media resources tagged as attachment-files
686
676
  const allMedia = await this.medplum.searchResources('Media', {
687
677
  _tag: 'attachment-file',
@@ -694,7 +684,7 @@ export class MedplumNotificationBackend {
694
684
  const referencedIds = new Set();
695
685
  for (const comm of communications) {
696
686
  for (const payload of comm.payload || []) {
697
- const url = (_a = payload.contentAttachment) === null || _a === void 0 ? void 0 : _a.url;
687
+ const url = payload.contentAttachment?.url;
698
688
  if (url) {
699
689
  // Extract Media ID from URL (assuming format like Media/{id})
700
690
  const match = url.match(/Media\/([^/]+)/);
@@ -750,13 +740,13 @@ export class MedplumNotificationBackend {
750
740
  checksum: fileRecord.checksum,
751
741
  createdAt: fileRecord.createdAt,
752
742
  file: attachmentFile,
753
- description: payloadData === null || payloadData === void 0 ? void 0 : payloadData.description,
743
+ description: payloadData?.description,
754
744
  storageMetadata: fileRecord.storageMetadata,
755
745
  });
756
746
  }
757
747
  return attachments;
758
748
  }
759
- catch (_a) {
749
+ catch {
760
750
  return [];
761
751
  }
762
752
  }
@@ -764,8 +754,7 @@ export class MedplumNotificationBackend {
764
754
  const communication = await this.medplum.readResource('Communication', notificationId);
765
755
  // Check if attachment exists
766
756
  const hasAttachment = (communication.payload || []).some((p) => {
767
- var _a;
768
- const url = (_a = p.contentAttachment) === null || _a === void 0 ? void 0 : _a.url;
757
+ const url = p.contentAttachment?.url;
769
758
  return url && url.includes(attachmentId);
770
759
  });
771
760
  if (!hasAttachment) {
@@ -773,8 +762,7 @@ export class MedplumNotificationBackend {
773
762
  }
774
763
  // Filter out the attachment from payload
775
764
  const updatedPayload = (communication.payload || []).filter((p) => {
776
- var _a;
777
- const url = (_a = p.contentAttachment) === null || _a === void 0 ? void 0 : _a.url;
765
+ const url = p.contentAttachment?.url;
778
766
  if (!url)
779
767
  return true;
780
768
  return !url.includes(attachmentId);
@@ -6,3 +6,4 @@ export declare class MedplumLogger implements BaseLogger {
6
6
  error(message: string): void;
7
7
  warn(message: string): void;
8
8
  }
9
+ //# sourceMappingURL=medplum-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"medplum-logger.d.ts","sourceRoot":"","sources":["../src/medplum-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAC;AAE9E,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,MAAM,CAAiB;;IAM/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAG5B"}
@@ -0,0 +1,29 @@
1
+ import { BaseEmailTemplateRenderer } from 'vintasend';
2
+ import type { JsonObject } from 'vintasend/dist/types/json-values';
3
+ import type { DatabaseNotification } from 'vintasend/dist/types/notification';
4
+ import type { BaseNotificationTypeConfig } from 'vintasend/dist/types/notification-type-config';
5
+ import type { BaseLogger } from 'vintasend/dist/services/loggers/base-logger';
6
+ /**
7
+ * Custom email template renderer that compiles Pug templates from strings
8
+ * instead of reading from file paths.
9
+ *
10
+ * This is necessary for bot deployments where templates are embedded as constants
11
+ * rather than separate files.
12
+ */
13
+ export declare class PugInlineEmailTemplateRenderer<Config extends BaseNotificationTypeConfig> implements BaseEmailTemplateRenderer<Config> {
14
+ private templates;
15
+ private logger;
16
+ constructor(generatedTemplates: Record<string, string>);
17
+ /**
18
+ * Inject logger (called by VintaSend when logger exists)
19
+ */
20
+ injectLogger(logger: BaseLogger): void;
21
+ render(notification: DatabaseNotification<Config>, context: JsonObject): Promise<{
22
+ subject: string;
23
+ body: string;
24
+ }>;
25
+ }
26
+ export declare class PugInlineEmailTemplateRendererFactory<Config extends BaseNotificationTypeConfig> {
27
+ create(generatedTemplates: Record<string, string>): PugInlineEmailTemplateRenderer<Config>;
28
+ }
29
+ //# sourceMappingURL=pug-inline-email-template-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pug-inline-email-template-renderer.d.ts","sourceRoot":"","sources":["../src/pug-inline-email-template-renderer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAChG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAC;AAG9E;;;;;;GAMG;AACH,qBAAa,8BAA8B,CAAC,MAAM,SAAS,0BAA0B,CACnF,YAAW,yBAAyB,CAAC,MAAM,CAAC;IAE5C,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,MAAM,CAA2B;gBAE7B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIhC,MAAM,CACV,YAAY,EAAE,oBAAoB,CAAC,MAAM,CAAC,EAC1C,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CA6C9C;AAED,qBAAa,qCAAqC,CAAC,MAAM,SAAS,0BAA0B;IAC1F,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,8BAA8B,CAAC,MAAM,CAAC;CAG3F"}
@@ -0,0 +1,68 @@
1
+ import * as pug from 'pug';
2
+ /**
3
+ * Custom email template renderer that compiles Pug templates from strings
4
+ * instead of reading from file paths.
5
+ *
6
+ * This is necessary for bot deployments where templates are embedded as constants
7
+ * rather than separate files.
8
+ */
9
+ export class PugInlineEmailTemplateRenderer {
10
+ constructor(generatedTemplates) {
11
+ this.logger = null;
12
+ this.templates = generatedTemplates;
13
+ }
14
+ /**
15
+ * Inject logger (called by VintaSend when logger exists)
16
+ */
17
+ injectLogger(logger) {
18
+ this.logger = logger;
19
+ }
20
+ async render(notification, context) {
21
+ // Check if body template is provided
22
+ const bodyTemplateKey = notification.bodyTemplate;
23
+ if (!bodyTemplateKey) {
24
+ throw new Error('Body template is required');
25
+ }
26
+ if (!(bodyTemplateKey in this.templates)) {
27
+ throw new Error(`Body template "${bodyTemplateKey}" not found in templates`);
28
+ }
29
+ // Check if subject template is provided
30
+ const subjectTemplateKey = notification.subjectTemplate;
31
+ if (!subjectTemplateKey) {
32
+ throw new Error('Subject template is required');
33
+ }
34
+ if (!(subjectTemplateKey in this.templates)) {
35
+ throw new Error(`Subject template "${subjectTemplateKey}" not found in templates`);
36
+ }
37
+ let body;
38
+ try {
39
+ // Compile and render the body template from string
40
+ const bodyTemplate = pug.compile(this.templates[bodyTemplateKey]);
41
+ body = bodyTemplate(context);
42
+ }
43
+ catch (error) {
44
+ if (this.logger) {
45
+ this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering body template');
46
+ }
47
+ throw error;
48
+ }
49
+ let subject;
50
+ try {
51
+ // Compile and render the subject template from string
52
+ const subjectTemplate = pug.compile(this.templates[subjectTemplateKey]);
53
+ subject = subjectTemplate(context);
54
+ }
55
+ catch (error) {
56
+ if (this.logger) {
57
+ this.logger.error('[PugInlineEmailTemplateRenderer] Error rendering subject template');
58
+ }
59
+ throw error;
60
+ }
61
+ return { subject, body };
62
+ }
63
+ }
64
+ export class PugInlineEmailTemplateRendererFactory {
65
+ create(generatedTemplates) {
66
+ return new PugInlineEmailTemplateRenderer(generatedTemplates);
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=compile-pug-templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-pug-templates.d.ts","sourceRoot":"","sources":["../../src/scripts/compile-pug-templates.ts"],"names":[],"mappings":""}
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ /**
5
+ * Recursively finds all .pug files in a directory
6
+ */
7
+ function findPugFiles(dir, baseDir, files = new Map()) {
8
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
9
+ for (const entry of entries) {
10
+ const fullPath = path.join(dir, entry.name);
11
+ if (entry.isDirectory()) {
12
+ // Recursively search subdirectories
13
+ findPugFiles(fullPath, baseDir, files);
14
+ }
15
+ else if (entry.isFile() && entry.name.endsWith('.pug')) {
16
+ // Read the pug file content
17
+ const content = fs.readFileSync(fullPath, 'utf-8');
18
+ // Use relative path from base directory as the key
19
+ // Normalize to POSIX-style path (forward slashes) for cross-platform consistency
20
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
21
+ files.set(relativePath, content);
22
+ }
23
+ }
24
+ return files;
25
+ }
26
+ /**
27
+ * Main function to compile pug templates to JSON
28
+ */
29
+ function compilePugTemplates(inputDir = './templates', outputFile = 'compiled-templates.json') {
30
+ // Validate input directory exists
31
+ if (!fs.existsSync(inputDir)) {
32
+ console.error(`Error: Directory "${inputDir}" does not exist.`);
33
+ process.exit(1);
34
+ }
35
+ if (!fs.statSync(inputDir).isDirectory()) {
36
+ console.error(`Error: "${inputDir}" is not a directory.`);
37
+ process.exit(1);
38
+ }
39
+ console.log(`Searching for .pug files in: ${inputDir}`);
40
+ // Find all pug files
41
+ const pugFiles = findPugFiles(inputDir, inputDir);
42
+ console.log(`Found ${pugFiles.size} .pug file(s)`);
43
+ // Convert Map to plain object
44
+ const result = {};
45
+ for (const [filePath, content] of pugFiles.entries()) {
46
+ result[filePath] = content;
47
+ console.log(` - ${filePath}`);
48
+ }
49
+ // Write to output file
50
+ const outputContent = JSON.stringify(result, null, 2);
51
+ fs.writeFileSync(outputFile, outputContent, 'utf-8');
52
+ console.log(`\nSuccessfully compiled templates to: ${outputFile}`);
53
+ }
54
+ // Parse command line arguments
55
+ const args = process.argv.slice(2);
56
+ if (args.includes('--help') || args.includes('-h')) {
57
+ console.log('Usage: compile-pug-templates [input-directory] [output-file]');
58
+ console.log('');
59
+ console.log('Arguments:');
60
+ console.log(' input-directory Directory containing .pug templates (default: ./templates)');
61
+ console.log(' output-file Output JSON file path (default: compiled-templates.json)');
62
+ console.log('');
63
+ console.log('Examples:');
64
+ console.log(' compile-pug-templates');
65
+ console.log(' compile-pug-templates ./templates');
66
+ console.log(' compile-pug-templates ./templates ./compiled-templates.json');
67
+ process.exit(0);
68
+ }
69
+ const [inputDir, outputFile] = args;
70
+ // Run the compilation
71
+ compilePugTemplates(inputDir, outputFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend-medplum",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -8,26 +8,35 @@
8
8
  "prepublishOnly": "npm run build",
9
9
  "test": "jest",
10
10
  "test:watch": "jest --watch",
11
- "test:coverage": "jest --coverage"
11
+ "test:coverage": "jest --coverage",
12
+ "compile-templates": "ts-node src/scripts/compile-pug-templates.ts"
12
13
  },
13
14
  "files": [
14
- "dist"
15
+ "dist",
16
+ "src/scripts/compile-pug-templates.ts"
15
17
  ],
18
+ "bin": {
19
+ "compile-pug-templates": "dist/scripts/compile-pug-templates.js"
20
+ },
16
21
  "author": "Hugo Bessa",
17
22
  "license": "MIT",
18
23
  "peerDependencies": {
19
24
  "@medplum/core": "^5.0.10",
20
25
  "@medplum/fhirtypes": "^5.0.10",
21
26
  "pdfmake": "^0.2.23",
22
- "vintasend": "^0.4.0"
27
+ "pug": "^3.0.3",
28
+ "vintasend": "^0.4.3"
23
29
  },
24
30
  "devDependencies": {
25
31
  "@medplum/definitions": "^5.0.10",
26
32
  "@medplum/mock": "^5.0.10",
27
33
  "@types/jest": "^30.0.0",
34
+ "@types/node": "^22.0.0",
28
35
  "@types/pdfmake": "^0.2.12",
36
+ "@types/pug": "^2.0.10",
29
37
  "jest": "^30.2.0",
30
38
  "ts-jest": "^29.4.6",
39
+ "ts-node": "^10.9.2",
31
40
  "typescript": "^5.9.3"
32
41
  }
33
42
  }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ interface PugTemplatesMap {
6
+ [key: string]: string;
7
+ }
8
+
9
+ /**
10
+ * Recursively finds all .pug files in a directory
11
+ */
12
+ function findPugFiles(dir: string, baseDir: string, files: Map<string, string> = new Map()): Map<string, string> {
13
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
14
+
15
+ for (const entry of entries) {
16
+ const fullPath = path.join(dir, entry.name);
17
+
18
+ if (entry.isDirectory()) {
19
+ // Recursively search subdirectories
20
+ findPugFiles(fullPath, baseDir, files);
21
+ } else if (entry.isFile() && entry.name.endsWith('.pug')) {
22
+ // Read the pug file content
23
+ const content = fs.readFileSync(fullPath, 'utf-8');
24
+ // Use relative path from base directory as the key
25
+ // Normalize to POSIX-style path (forward slashes) for cross-platform consistency
26
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
27
+ files.set(relativePath, content);
28
+ }
29
+ }
30
+
31
+ return files;
32
+ }
33
+
34
+ /**
35
+ * Main function to compile pug templates to JSON
36
+ */
37
+ function compilePugTemplates(inputDir: string = './templates', outputFile: string = 'compiled-templates.json'): void {
38
+ // Validate input directory exists
39
+ if (!fs.existsSync(inputDir)) {
40
+ console.error(`Error: Directory "${inputDir}" does not exist.`);
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!fs.statSync(inputDir).isDirectory()) {
45
+ console.error(`Error: "${inputDir}" is not a directory.`);
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log(`Searching for .pug files in: ${inputDir}`);
50
+
51
+ // Find all pug files
52
+ const pugFiles = findPugFiles(inputDir, inputDir);
53
+
54
+ console.log(`Found ${pugFiles.size} .pug file(s)`);
55
+
56
+ // Convert Map to plain object
57
+ const result: PugTemplatesMap = {};
58
+ for (const [filePath, content] of pugFiles.entries()) {
59
+ result[filePath] = content;
60
+ console.log(` - ${filePath}`);
61
+ }
62
+
63
+ // Write to output file
64
+ const outputContent = JSON.stringify(result, null, 2);
65
+ fs.writeFileSync(outputFile, outputContent, 'utf-8');
66
+
67
+ console.log(`\nSuccessfully compiled templates to: ${outputFile}`);
68
+ }
69
+
70
+ // Parse command line arguments
71
+ const args = process.argv.slice(2);
72
+
73
+ if (args.includes('--help') || args.includes('-h')) {
74
+ console.log('Usage: compile-pug-templates [input-directory] [output-file]');
75
+ console.log('');
76
+ console.log('Arguments:');
77
+ console.log(' input-directory Directory containing .pug templates (default: ./templates)');
78
+ console.log(' output-file Output JSON file path (default: compiled-templates.json)');
79
+ console.log('');
80
+ console.log('Examples:');
81
+ console.log(' compile-pug-templates');
82
+ console.log(' compile-pug-templates ./templates');
83
+ console.log(' compile-pug-templates ./templates ./compiled-templates.json');
84
+ process.exit(0);
85
+ }
86
+
87
+ const [inputDir, outputFile] = args;
88
+
89
+ // Run the compilation
90
+ compilePugTemplates(inputDir, outputFile);