vintasend-medplum 0.5.1 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import { MedplumClient } from '@medplum/core';
2
2
  import type { BaseAttachmentManager } from 'vintasend/dist/services/attachment-manager/base-attachment-manager';
3
3
  import type { BaseLogger } from 'vintasend/dist/services/loggers/base-logger';
4
- import type { BaseNotificationBackend } from 'vintasend/dist/services/notification-backends/base-notification-backend';
4
+ import type { BaseNotificationBackend, NotificationFilter } from 'vintasend/dist/services/notification-backends/base-notification-backend';
5
5
  import type { AttachmentFileRecord, StoredAttachment } from 'vintasend/dist/types/attachment';
6
6
  import type { InputJsonValue } from 'vintasend/dist/types/json-values';
7
7
  import type { AnyDatabaseNotification, DatabaseNotification, NotificationInput } from 'vintasend/dist/types/notification';
@@ -24,6 +24,29 @@ export declare class MedplumNotificationBackend<Config extends BaseNotificationT
24
24
  * Inject logger for debugging and monitoring
25
25
  */
26
26
  injectLogger(logger: BaseLogger): void;
27
+ /**
28
+ * Get the filter capabilities supported by Medplum backend.
29
+ * Medplum has limitations with OR operators and negation on date ranges.
30
+ */
31
+ getFilterCapabilities(): {
32
+ 'logical.and': boolean;
33
+ 'logical.or': boolean;
34
+ 'logical.not': boolean;
35
+ 'logical.notNested': boolean;
36
+ 'fields.status': boolean;
37
+ 'fields.notificationType': boolean;
38
+ 'fields.adapterUsed': boolean;
39
+ 'fields.userId': boolean;
40
+ 'fields.bodyTemplate': boolean;
41
+ 'fields.subjectTemplate': boolean;
42
+ 'fields.contextName': boolean;
43
+ 'fields.sendAfterRange': boolean;
44
+ 'fields.createdAtRange': boolean;
45
+ 'fields.sentAtRange': boolean;
46
+ 'negation.sendAfterRange': boolean;
47
+ 'negation.createdAtRange': boolean;
48
+ 'negation.sentAtRange': boolean;
49
+ };
27
50
  /**
28
51
  * Get attachment manager with null check
29
52
  */
@@ -37,6 +60,11 @@ export declare class MedplumNotificationBackend<Config extends BaseNotificationT
37
60
  getFutureNotificationsFromUser(referenceString: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
38
61
  persistNotification(notification: NotificationInput<Config>): Promise<DatabaseNotification<Config>>;
39
62
  persistNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<DatabaseNotification<Config>, 'id'>>): Promise<DatabaseNotification<Config>>;
63
+ /**
64
+ * Update FHIR identifiers from a partial notification update.
65
+ * Replaces identifier values for known systems when new values are provided.
66
+ */
67
+ private updateIdentifiers;
40
68
  getAllNotifications(): Promise<AnyDatabaseNotification<Config>[]>;
41
69
  getNotifications(page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
42
70
  bulkPersistNotifications(notifications: Omit<NotificationInput<Config>, 'id'>[]): Promise<Config['NotificationIdType'][]>;
@@ -48,12 +76,65 @@ export declare class MedplumNotificationBackend<Config extends BaseNotificationT
48
76
  filterAllInAppUnreadNotifications(refenrenceString: Config['UserIdType']): Promise<DatabaseNotification<Config>[]>;
49
77
  filterInAppUnreadNotifications(refenrenceString: Config['UserIdType'], page: number, pageSize: number): Promise<DatabaseNotification<Config>[]>;
50
78
  getUserEmailFromNotification(notificationId: Config['NotificationIdType']): Promise<string | undefined>;
51
- storeContextUsed(notificationId: Config['NotificationIdType'], context: InputJsonValue): Promise<void>;
79
+ storeAdapterAndContextUsed(notificationId: Config['NotificationIdType'], adapterKey: string, context: InputJsonValue): Promise<void>;
52
80
  persistOneOffNotification(notification: Omit<OneOffNotificationInput<Config>, 'id'>): Promise<DatabaseOneOffNotification<Config>>;
53
81
  persistOneOffNotificationUpdate(notificationId: Config['NotificationIdType'], notification: Partial<Omit<DatabaseOneOffNotification<Config>, 'id'>>): Promise<DatabaseOneOffNotification<Config>>;
54
82
  getOneOffNotification(notificationId: Config['NotificationIdType'], forUpdate: boolean): Promise<DatabaseOneOffNotification<Config> | null>;
55
83
  getAllOneOffNotifications(): Promise<DatabaseOneOffNotification<Config>[]>;
56
84
  getOneOffNotifications(page: number, pageSize: number): Promise<DatabaseOneOffNotification<Config>[]>;
85
+ /**
86
+ * Filter notifications using composable query filters.
87
+ * All filtering is delegated to the FHIR server via search parameters.
88
+ *
89
+ * **Supported filter fields** (translated to FHIR search parameters):
90
+ * - `status` → `Communication.status`
91
+ * - `notificationType` → `_tag`
92
+ * - `contextName` → `_tag`
93
+ * - `userId` → `recipient`
94
+ * - `adapterUsed` → `identifier` (system: vintasend adapter-used)
95
+ * - `bodyTemplate` → `identifier` (system: vintasend body-template)
96
+ * - `subjectTemplate` → `identifier` (system: vintasend subject-template)
97
+ * - `sendAfterRange` → `sent` (date comparators)
98
+ * - `sentAtRange` → `sent` (date comparators)
99
+ * - `createdAtRange` → `_lastUpdated` (date comparators)
100
+ *
101
+ * **Logical operators:**
102
+ * - `AND` — merges FHIR search parameters from sub-filters
103
+ * - `OR` — not supported by FHIR search (throws)
104
+ * - `NOT` — uses FHIR `:not` modifier (status, notificationType, userId only)
105
+ */
106
+ filterNotifications(filter: NotificationFilter<Config>, page: number, pageSize: number): Promise<AnyDatabaseNotification<Config>[]>;
107
+ /**
108
+ * Ensure the `_tag` parameter always includes `notification`.
109
+ */
110
+ private ensureNotificationTag;
111
+ /**
112
+ * Recursively build FHIR search parameters from a NotificationFilter tree.
113
+ * Handles field filters, AND, and NOT. OR is handled at the caller level.
114
+ */
115
+ private buildFhirSearchParams;
116
+ /**
117
+ * Merge FHIR search parameters from multiple AND sub-filters.
118
+ */
119
+ private mergeAndFilters;
120
+ /**
121
+ * Convert a leaf field filter to FHIR search parameters.
122
+ * Throws for fields that are not queryable via FHIR search.
123
+ */
124
+ private fieldFilterToFhirParams;
125
+ /**
126
+ * Negate a simple field filter using the FHIR `:not` modifier.
127
+ * Only supports `status`, `notificationType`, and `userId`.
128
+ */
129
+ private negateFilter;
130
+ /**
131
+ * Throw if the filter uses fields not queryable via FHIR search.
132
+ */
133
+ private throwIfUnsupportedField;
134
+ /**
135
+ * Map VintaSend NotificationStatus to FHIR Communication status.
136
+ */
137
+ private mapStatusToFhirStatus;
57
138
  /**
58
139
  * Store an attachment file record in the backend's database (Media resource).
59
140
  * This is called after the AttachmentManager uploads a file and returns storageIdentifiers.
@@ -1 +1 @@
1
- {"version":3,"file":"medplum-backend.d.ts","sourceRoot":"","sources":["../src/medplum-backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"medplum-backend.d.ts","sourceRoot":"","sources":["../src/medplum-backend.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,10 @@
1
+ import { isFieldFilter } from 'vintasend/dist/services/notification-backends/base-notification-backend';
2
+ /** FHIR identifier systems used by MedplumNotificationBackend for searchable fields. */
3
+ const IDENTIFIER_SYSTEMS = {
4
+ bodyTemplate: 'http://vintasend.com/fhir/body-template',
5
+ subjectTemplate: 'http://vintasend.com/fhir/subject-template',
6
+ adapterUsed: 'http://vintasend.com/fhir/adapter-used',
7
+ };
1
8
  export class MedplumNotificationBackend {
2
9
  constructor(medplum, options = {
3
10
  emailNotificationSubjectExtensionUrl: 'http://vintasend.com/fhir/StructureDefinition/email-notification-subject',
@@ -17,6 +24,31 @@ export class MedplumNotificationBackend {
17
24
  injectLogger(logger) {
18
25
  this.logger = logger;
19
26
  }
27
+ /**
28
+ * Get the filter capabilities supported by Medplum backend.
29
+ * Medplum has limitations with OR operators and negation on date ranges.
30
+ */
31
+ getFilterCapabilities() {
32
+ return {
33
+ 'logical.and': true,
34
+ 'logical.or': false,
35
+ 'logical.not': true,
36
+ 'logical.notNested': false,
37
+ 'fields.status': true,
38
+ 'fields.notificationType': true,
39
+ 'fields.adapterUsed': true,
40
+ 'fields.userId': true,
41
+ 'fields.bodyTemplate': true,
42
+ 'fields.subjectTemplate': true,
43
+ 'fields.contextName': true,
44
+ 'fields.sendAfterRange': true,
45
+ 'fields.createdAtRange': true,
46
+ 'fields.sentAtRange': true,
47
+ 'negation.sendAfterRange': false,
48
+ 'negation.createdAtRange': false,
49
+ 'negation.sentAtRange': false,
50
+ };
51
+ }
20
52
  /**
21
53
  * Get attachment manager with null check
22
54
  */
@@ -39,6 +71,15 @@ export class MedplumNotificationBackend {
39
71
  const emailOrPhone = emailOrPhoneExtension?.valueString;
40
72
  const firstName = firstNameExtension?.valueString;
41
73
  const lastName = lastNameExtension?.valueString;
74
+ // Extract contextUsed from extension
75
+ const contextUsedExtension = communication.extension?.find((ext) => ext.url === 'http://vintasend.com/fhir/StructureDefinition/contextUsed');
76
+ const contextUsed = contextUsedExtension?.valueString
77
+ ? JSON.parse(contextUsedExtension.valueString)
78
+ : null;
79
+ // Extract searchable fields from identifiers
80
+ const bodyTemplate = communication.identifier?.find((id) => id.system === IDENTIFIER_SYSTEMS.bodyTemplate)?.value || communication.payload?.[0]?.contentString || '';
81
+ const subjectTemplateFromId = communication.identifier?.find((id) => id.system === IDENTIFIER_SYSTEMS.subjectTemplate)?.value || subjectTemplate;
82
+ const adapterUsed = communication.identifier?.find((id) => id.system === IDENTIFIER_SYSTEMS.adapterUsed)?.value || null;
42
83
  const baseNotification = {
43
84
  id: notificationId,
44
85
  notificationType: communication.meta?.tag?.[2]?.code,
@@ -46,12 +87,12 @@ export class MedplumNotificationBackend {
46
87
  contextName: communication.meta?.tag?.[1]?.code,
47
88
  contextParameters: JSON.parse(communication.note?.[0]?.text || '{}'),
48
89
  sendAfter: communication.sent ? new Date(communication.sent) : null,
49
- bodyTemplate: communication.payload?.[0]?.contentString || '',
50
- subjectTemplate: subjectTemplate,
90
+ bodyTemplate,
91
+ subjectTemplate: subjectTemplateFromId,
51
92
  extraParams: {},
52
93
  status: communication.status === 'completed' ? 'SENT' : communication.status === 'stopped' ? 'FAILED' : 'PENDING_SEND',
53
- contextUsed: null,
54
- adapterUsed: null,
94
+ contextUsed,
95
+ adapterUsed,
55
96
  sentAt: communication.status === 'completed' ? new Date(communication.sent || new Date()) : null,
56
97
  readAt: null,
57
98
  createdAt: new Date(communication.meta?.lastUpdated || new Date()),
@@ -157,12 +198,19 @@ export class MedplumNotificationBackend {
157
198
  payload.push(...attachmentPayload);
158
199
  }
159
200
  }
201
+ const identifiers = [
202
+ { system: IDENTIFIER_SYSTEMS.bodyTemplate, value: notification.bodyTemplate },
203
+ ];
204
+ if (notification.subjectTemplate) {
205
+ identifiers.push({ system: IDENTIFIER_SYSTEMS.subjectTemplate, value: notification.subjectTemplate });
206
+ }
160
207
  const communication = {
161
208
  resourceType: 'Communication',
162
209
  status: 'in-progress',
163
210
  sent: notification.sendAfter?.toISOString(),
164
211
  topic: { text: notification.title || undefined },
165
212
  payload,
213
+ identifier: identifiers,
166
214
  recipient: [{ reference: notification.userId }],
167
215
  note: [{ text: JSON.stringify(notification.contextParameters) }],
168
216
  meta: {
@@ -185,10 +233,14 @@ export class MedplumNotificationBackend {
185
233
  }
186
234
  async persistNotificationUpdate(notificationId, notification) {
187
235
  const existing = await this.medplum.readResource('Communication', notificationId);
236
+ // Update identifiers for searchable fields when they change
237
+ const existingIdentifiers = existing.identifier || [];
238
+ const updatedIdentifiers = this.updateIdentifiers(existingIdentifiers, notification);
188
239
  const updated = {
189
240
  ...existing,
190
241
  ...(notification.status === 'SENT' && { status: 'completed' }),
191
242
  ...(notification.status === 'FAILED' && { status: 'stopped' }),
243
+ identifier: updatedIdentifiers,
192
244
  meta: {
193
245
  ...existing.meta,
194
246
  lastUpdated: new Date().toISOString(),
@@ -197,6 +249,32 @@ export class MedplumNotificationBackend {
197
249
  const result = await this.medplum.updateResource(updated);
198
250
  return this.mapToDatabaseNotification(result);
199
251
  }
252
+ /**
253
+ * Update FHIR identifiers from a partial notification update.
254
+ * Replaces identifier values for known systems when new values are provided.
255
+ */
256
+ updateIdentifiers(existing, notification) {
257
+ const updated = [...existing];
258
+ const upsert = (system, value) => {
259
+ if (value === undefined)
260
+ return;
261
+ const idx = updated.findIndex((id) => id.system === system);
262
+ if (value === null) {
263
+ if (idx >= 0)
264
+ updated.splice(idx, 1);
265
+ }
266
+ else if (idx >= 0) {
267
+ updated[idx] = { ...updated[idx], system, value };
268
+ }
269
+ else {
270
+ updated.push({ system, value });
271
+ }
272
+ };
273
+ upsert(IDENTIFIER_SYSTEMS.bodyTemplate, notification.bodyTemplate);
274
+ upsert(IDENTIFIER_SYSTEMS.subjectTemplate, notification.subjectTemplate);
275
+ upsert(IDENTIFIER_SYSTEMS.adapterUsed, notification.adapterUsed);
276
+ return updated;
277
+ }
200
278
  async getAllNotifications() {
201
279
  const communications = await this.medplum.searchResources('Communication', {
202
280
  _tag: 'notification',
@@ -214,6 +292,12 @@ export class MedplumNotificationBackend {
214
292
  async bulkPersistNotifications(notifications) {
215
293
  const ids = [];
216
294
  for (const notification of notifications) {
295
+ const bulkIdentifiers = [
296
+ { system: IDENTIFIER_SYSTEMS.bodyTemplate, value: notification.bodyTemplate },
297
+ ];
298
+ if (notification.subjectTemplate) {
299
+ bulkIdentifiers.push({ system: IDENTIFIER_SYSTEMS.subjectTemplate, value: notification.subjectTemplate });
300
+ }
217
301
  const communication = {
218
302
  resourceType: 'Communication',
219
303
  status: 'in-progress',
@@ -229,6 +313,7 @@ export class MedplumNotificationBackend {
229
313
  },
230
314
  ]
231
315
  : [],
316
+ identifier: bulkIdentifiers,
232
317
  recipient: [{ reference: notification.userId }],
233
318
  note: [{ text: JSON.stringify(notification.contextParameters) }],
234
319
  meta: {
@@ -349,11 +434,23 @@ export class MedplumNotificationBackend {
349
434
  return undefined;
350
435
  }
351
436
  }
352
- async storeContextUsed(notificationId, context) {
437
+ async storeAdapterAndContextUsed(notificationId, adapterKey, context) {
353
438
  const communication = await this.medplum.readResource('Communication', notificationId);
439
+ const contextUsedExtensionUrl = 'http://vintasend.com/fhir/StructureDefinition/contextUsed';
440
+ // Remove any existing contextUsed extension, then add the new one
441
+ const existingExtensions = (communication.extension || []).filter((ext) => ext.url !== contextUsedExtensionUrl);
442
+ // Upsert the adapterUsed identifier
443
+ const existingIdentifiers = (communication.identifier || []).filter((id) => id.system !== IDENTIFIER_SYSTEMS.adapterUsed);
354
444
  const updated = {
355
445
  ...communication,
356
- note: [{ text: JSON.stringify(context) }],
446
+ identifier: [
447
+ ...existingIdentifiers,
448
+ { system: IDENTIFIER_SYSTEMS.adapterUsed, value: adapterKey },
449
+ ],
450
+ extension: [
451
+ ...existingExtensions,
452
+ { url: contextUsedExtensionUrl, valueString: JSON.stringify(context) },
453
+ ],
357
454
  };
358
455
  await this.medplum.updateResource(updated);
359
456
  }
@@ -377,12 +474,19 @@ export class MedplumNotificationBackend {
377
474
  payload.push(...attachmentPayload);
378
475
  }
379
476
  }
477
+ const oneOffIdentifiers = [
478
+ { system: IDENTIFIER_SYSTEMS.bodyTemplate, value: notification.bodyTemplate },
479
+ ];
480
+ if (notification.subjectTemplate) {
481
+ oneOffIdentifiers.push({ system: IDENTIFIER_SYSTEMS.subjectTemplate, value: notification.subjectTemplate });
482
+ }
380
483
  const communication = {
381
484
  resourceType: 'Communication',
382
485
  status: 'in-progress',
383
486
  sent: notification.sendAfter?.toISOString(),
384
487
  topic: { text: notification.title || undefined },
385
488
  payload,
489
+ identifier: oneOffIdentifiers,
386
490
  note: [{ text: JSON.stringify(notification.contextParameters) }],
387
491
  extension: [
388
492
  { url: 'http://vintasend.com/fhir/StructureDefinition/emailOrPhone', valueString: notification.emailOrPhone },
@@ -408,10 +512,13 @@ export class MedplumNotificationBackend {
408
512
  }
409
513
  async persistOneOffNotificationUpdate(notificationId, notification) {
410
514
  const existing = await this.medplum.readResource('Communication', notificationId);
515
+ const existingIdentifiers = existing.identifier || [];
516
+ const updatedIdentifiers = this.updateIdentifiers(existingIdentifiers, notification);
411
517
  const updated = {
412
518
  ...existing,
413
519
  ...(notification.status === 'SENT' && { status: 'completed' }),
414
520
  ...(notification.status === 'FAILED' && { status: 'stopped' }),
521
+ identifier: updatedIdentifiers,
415
522
  meta: {
416
523
  ...existing.meta,
417
524
  lastUpdated: new Date().toISOString(),
@@ -444,6 +551,261 @@ export class MedplumNotificationBackend {
444
551
  });
445
552
  return communications.map((comm) => this.mapToDatabaseNotification(comm));
446
553
  }
554
+ /* Notification filtering methods */
555
+ /**
556
+ * Filter notifications using composable query filters.
557
+ * All filtering is delegated to the FHIR server via search parameters.
558
+ *
559
+ * **Supported filter fields** (translated to FHIR search parameters):
560
+ * - `status` → `Communication.status`
561
+ * - `notificationType` → `_tag`
562
+ * - `contextName` → `_tag`
563
+ * - `userId` → `recipient`
564
+ * - `adapterUsed` → `identifier` (system: vintasend adapter-used)
565
+ * - `bodyTemplate` → `identifier` (system: vintasend body-template)
566
+ * - `subjectTemplate` → `identifier` (system: vintasend subject-template)
567
+ * - `sendAfterRange` → `sent` (date comparators)
568
+ * - `sentAtRange` → `sent` (date comparators)
569
+ * - `createdAtRange` → `_lastUpdated` (date comparators)
570
+ *
571
+ * **Logical operators:**
572
+ * - `AND` — merges FHIR search parameters from sub-filters
573
+ * - `OR` — not supported by FHIR search (throws)
574
+ * - `NOT` — uses FHIR `:not` modifier (status, notificationType, userId only)
575
+ */
576
+ async filterNotifications(filter, page, pageSize) {
577
+ if ('or' in filter) {
578
+ throw new Error('OR filters are not supported by MedplumNotificationBackend (FHIR search does not support OR logic).');
579
+ }
580
+ const searchParams = this.buildFhirSearchParams(filter);
581
+ this.ensureNotificationTag(searchParams);
582
+ searchParams._count = pageSize.toString();
583
+ searchParams._offset = (page * pageSize).toString();
584
+ const communications = await this.medplum.searchResources('Communication', searchParams);
585
+ return communications.map((comm) => this.mapToDatabaseNotification(comm));
586
+ }
587
+ /**
588
+ * Ensure the `_tag` parameter always includes `notification`.
589
+ */
590
+ ensureNotificationTag(params) {
591
+ if (params._tag) {
592
+ if (!params._tag.split(',').includes('notification')) {
593
+ params._tag = `notification,${params._tag}`;
594
+ }
595
+ }
596
+ else {
597
+ params._tag = 'notification';
598
+ }
599
+ }
600
+ /**
601
+ * Recursively build FHIR search parameters from a NotificationFilter tree.
602
+ * Handles field filters, AND, and NOT. OR is handled at the caller level.
603
+ */
604
+ buildFhirSearchParams(filter) {
605
+ if ('or' in filter) {
606
+ throw new Error('OR filters are not supported by MedplumNotificationBackend (FHIR search does not support OR logic).');
607
+ }
608
+ if ('and' in filter) {
609
+ return this.mergeAndFilters(filter.and);
610
+ }
611
+ if ('not' in filter) {
612
+ return this.negateFilter(filter.not);
613
+ }
614
+ // Leaf field filter
615
+ return this.fieldFilterToFhirParams(filter);
616
+ }
617
+ /**
618
+ * Merge FHIR search parameters from multiple AND sub-filters.
619
+ */
620
+ mergeAndFilters(subFilters) {
621
+ const merged = {};
622
+ for (const sub of subFilters) {
623
+ const params = this.buildFhirSearchParams(sub);
624
+ for (const [key, value] of Object.entries(params)) {
625
+ if (key in merged && merged[key] !== value) {
626
+ throw new Error(`Conflicting values for FHIR search parameter '${key}' in AND filter: ` +
627
+ `'${merged[key]}' vs '${value}'. ` +
628
+ 'AND sub-filters must not set different values for the same search parameter.');
629
+ }
630
+ merged[key] = value;
631
+ }
632
+ }
633
+ return merged;
634
+ }
635
+ /**
636
+ * Convert a leaf field filter to FHIR search parameters.
637
+ * Throws for fields that are not queryable via FHIR search.
638
+ */
639
+ fieldFilterToFhirParams(filter) {
640
+ const params = {};
641
+ // Status → Communication.status (comma-separated = OR)
642
+ if (filter.status !== undefined) {
643
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
644
+ const fhirStatuses = statuses
645
+ .map((s) => this.mapStatusToFhirStatus(s))
646
+ .filter((s) => s !== null);
647
+ if (fhirStatuses.length > 0) {
648
+ params.status = fhirStatuses.join(',');
649
+ }
650
+ }
651
+ // Notification type → _tag (comma-separated = AND in FHIR tags)
652
+ if (filter.notificationType !== undefined) {
653
+ const types = Array.isArray(filter.notificationType)
654
+ ? filter.notificationType
655
+ : [filter.notificationType];
656
+ params._tag = types.join(',');
657
+ }
658
+ // contextName → _tag (appended to existing _tag)
659
+ if (filter.contextName !== undefined) {
660
+ params._tag = params._tag
661
+ ? `${params._tag},${filter.contextName}`
662
+ : filter.contextName;
663
+ }
664
+ // UserId → recipient
665
+ if (filter.userId !== undefined) {
666
+ params.recipient = filter.userId;
667
+ }
668
+ // adapterUsed → identifier search with system
669
+ if (filter.adapterUsed !== undefined) {
670
+ const adapters = Array.isArray(filter.adapterUsed)
671
+ ? filter.adapterUsed
672
+ : [filter.adapterUsed];
673
+ params.identifier = adapters
674
+ .map((a) => `${IDENTIFIER_SYSTEMS.adapterUsed}|${a}`)
675
+ .join(',');
676
+ }
677
+ // bodyTemplate → identifier search with system
678
+ if (filter.bodyTemplate !== undefined) {
679
+ params.identifier = params.identifier
680
+ ? `${params.identifier},${IDENTIFIER_SYSTEMS.bodyTemplate}|${filter.bodyTemplate}`
681
+ : `${IDENTIFIER_SYSTEMS.bodyTemplate}|${filter.bodyTemplate}`;
682
+ }
683
+ // subjectTemplate → identifier search with system
684
+ if (filter.subjectTemplate !== undefined) {
685
+ params.identifier = params.identifier
686
+ ? `${params.identifier},${IDENTIFIER_SYSTEMS.subjectTemplate}|${filter.subjectTemplate}`
687
+ : `${IDENTIFIER_SYSTEMS.subjectTemplate}|${filter.subjectTemplate}`;
688
+ }
689
+ // sendAfterRange → sent date comparators
690
+ if (filter.sendAfterRange) {
691
+ if (filter.sendAfterRange.from) {
692
+ params['sent:ge'] = filter.sendAfterRange.from.toISOString();
693
+ }
694
+ if (filter.sendAfterRange.to) {
695
+ params['sent:le'] = filter.sendAfterRange.to.toISOString();
696
+ }
697
+ }
698
+ // sentAtRange → sent date comparators
699
+ if (filter.sentAtRange) {
700
+ if (filter.sentAtRange.from) {
701
+ params['sent:ge'] = filter.sentAtRange.from.toISOString();
702
+ }
703
+ if (filter.sentAtRange.to) {
704
+ params['sent:le'] = filter.sentAtRange.to.toISOString();
705
+ }
706
+ }
707
+ // createdAtRange → _lastUpdated (best approximation in FHIR)
708
+ if (filter.createdAtRange) {
709
+ if (filter.createdAtRange.from) {
710
+ params['_lastUpdated:ge'] = filter.createdAtRange.from.toISOString();
711
+ }
712
+ if (filter.createdAtRange.to) {
713
+ params['_lastUpdated:le'] = filter.createdAtRange.to.toISOString();
714
+ }
715
+ }
716
+ return params;
717
+ }
718
+ /**
719
+ * Negate a simple field filter using the FHIR `:not` modifier.
720
+ * Only supports `status`, `notificationType`, and `userId`.
721
+ */
722
+ negateFilter(inner) {
723
+ if (!isFieldFilter(inner)) {
724
+ throw new Error('NOT filters are only supported for simple field filters in MedplumNotificationBackend.');
725
+ }
726
+ this.throwIfUnsupportedField(inner);
727
+ const params = {};
728
+ const filter = inner;
729
+ if (filter.status !== undefined) {
730
+ const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
731
+ const fhirStatuses = statuses
732
+ .map((s) => this.mapStatusToFhirStatus(s))
733
+ .filter((s) => s !== null);
734
+ if (fhirStatuses.length > 0) {
735
+ params['status:not'] = fhirStatuses.join(',');
736
+ }
737
+ }
738
+ if (filter.notificationType !== undefined) {
739
+ const types = Array.isArray(filter.notificationType)
740
+ ? filter.notificationType
741
+ : [filter.notificationType];
742
+ params['_tag:not'] = types.join(',');
743
+ }
744
+ if (filter.contextName !== undefined) {
745
+ params['_tag:not'] = params['_tag:not']
746
+ ? `${params['_tag:not']},${filter.contextName}`
747
+ : filter.contextName;
748
+ }
749
+ if (filter.userId !== undefined) {
750
+ params['recipient:not'] = filter.userId;
751
+ }
752
+ if (filter.adapterUsed !== undefined) {
753
+ const adapters = Array.isArray(filter.adapterUsed)
754
+ ? filter.adapterUsed
755
+ : [filter.adapterUsed];
756
+ params['identifier:not'] = adapters
757
+ .map((a) => `${IDENTIFIER_SYSTEMS.adapterUsed}|${a}`)
758
+ .join(',');
759
+ }
760
+ if (filter.bodyTemplate !== undefined) {
761
+ params['identifier:not'] = params['identifier:not']
762
+ ? `${params['identifier:not']},${IDENTIFIER_SYSTEMS.bodyTemplate}|${filter.bodyTemplate}`
763
+ : `${IDENTIFIER_SYSTEMS.bodyTemplate}|${filter.bodyTemplate}`;
764
+ }
765
+ if (filter.subjectTemplate !== undefined) {
766
+ params['identifier:not'] = params['identifier:not']
767
+ ? `${params['identifier:not']},${IDENTIFIER_SYSTEMS.subjectTemplate}|${filter.subjectTemplate}`
768
+ : `${IDENTIFIER_SYSTEMS.subjectTemplate}|${filter.subjectTemplate}`;
769
+ }
770
+ if (filter.sentAtRange) {
771
+ throw new Error('NOT filter on sentAtRange is not supported by MedplumNotificationBackend.');
772
+ }
773
+ if (filter.sendAfterRange) {
774
+ throw new Error('NOT filter on sendAfterRange is not supported by MedplumNotificationBackend.');
775
+ }
776
+ if (filter.createdAtRange) {
777
+ throw new Error('NOT filter on createdAtRange is not supported by MedplumNotificationBackend.');
778
+ }
779
+ if (Object.keys(params).length === 0) {
780
+ throw new Error('NOT filter must contain at least one supported negatable field (status, notificationType, contextName, userId, adapterUsed, bodyTemplate, subjectTemplate).');
781
+ }
782
+ return params;
783
+ }
784
+ /**
785
+ * Throw if the filter uses fields not queryable via FHIR search.
786
+ */
787
+ throwIfUnsupportedField(_filter) {
788
+ // All fields in the current NotificationFilterFields are supported.
789
+ // This method is kept as a guard for future additions.
790
+ }
791
+ /**
792
+ * Map VintaSend NotificationStatus to FHIR Communication status.
793
+ */
794
+ mapStatusToFhirStatus(status) {
795
+ switch (status) {
796
+ case 'PENDING_SEND':
797
+ return 'in-progress';
798
+ case 'SENT':
799
+ case 'READ':
800
+ return 'completed';
801
+ case 'FAILED':
802
+ return 'stopped';
803
+ case 'CANCELLED':
804
+ return 'not-done';
805
+ default:
806
+ return null;
807
+ }
808
+ }
447
809
  /* Attachment management methods */
448
810
  /**
449
811
  * Store an attachment file record in the backend's database (Media resource).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintasend-medplum",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -25,7 +25,7 @@
25
25
  "@medplum/fhirtypes": "^5.0.10",
26
26
  "pdfmake": "^0.2.23",
27
27
  "pug": "^3.0.3",
28
- "vintasend": "^0.5.1"
28
+ "vintasend": "^0.6.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@medplum/definitions": "^5.0.10",