@@ -510,11 +510,12 @@ export class MedplumNotificationBackend<Config extends BaseNotificationTypeConfi
510510 async filterAllInAppUnreadNotifications (
511511 refenrenceString : Config [ 'UserIdType' ]
512512 ) : Promise < DatabaseNotification < Config > [ ] > {
513- const communications = await this . medplum . searchResources ( 'Communication' , {
514- status : 'completed' ,
515- _tag : 'notification,in-app' ,
516- recipient : refenrenceString ,
517- } ) ;
513+ const communications = await this . medplum . searchResources ( 'Communication' , [
514+ [ 'status' , 'completed' ] ,
515+ [ '_tag' , 'notification' ] ,
516+ [ '_tag' , 'in-app' ] ,
517+ [ 'recipient' , refenrenceString as string ] ,
518+ ] ) ;
518519 return communications
519520 . map ( ( comm ) => this . mapToDatabaseNotification ( comm ) )
520521 . filter ( ( notif ) : notif is DatabaseNotification < Config > => 'userId' in notif && ! notif . readAt ) ;
@@ -525,13 +526,14 @@ export class MedplumNotificationBackend<Config extends BaseNotificationTypeConfi
525526 page : number ,
526527 pageSize : number
527528 ) : Promise < DatabaseNotification < Config > [ ] > {
528- const communications = await this . medplum . searchResources ( 'Communication' , {
529- status : 'completed' ,
530- _tag : 'notification,in-app' ,
531- recipient : refenrenceString ,
532- _count : pageSize . toString ( ) ,
533- _offset : ( page * pageSize ) . toString ( ) ,
534- } ) ;
529+ const communications = await this . medplum . searchResources ( 'Communication' , [
530+ [ 'status' , 'completed' ] ,
531+ [ '_tag' , 'notification' ] ,
532+ [ '_tag' , 'in-app' ] ,
533+ [ 'recipient' , refenrenceString as string ] ,
534+ [ '_count' , pageSize . toString ( ) ] ,
535+ [ '_offset' , ( page * pageSize ) . toString ( ) ] ,
536+ ] ) ;
535537 return communications
536538 . map ( ( comm ) => this . mapToDatabaseNotification ( comm ) )
537539 . filter ( ( notif ) : notif is DatabaseNotification < Config > => 'userId' in notif && ! notif . readAt ) ;
@@ -692,21 +694,23 @@ export class MedplumNotificationBackend<Config extends BaseNotificationTypeConfi
692694 }
693695
694696 async getAllOneOffNotifications ( ) : Promise < DatabaseOneOffNotification < Config > [ ] > {
695- const communications = await this . medplum . searchResources ( 'Communication' , {
696- _tag : 'notification,one-off' ,
697- } ) ;
697+ const communications = await this . medplum . searchResources ( 'Communication' , [
698+ [ '_tag' , 'notification' ] ,
699+ [ '_tag' , 'one-off' ] ,
700+ ] ) ;
698701 return communications . map ( ( comm ) => this . mapToDatabaseNotification ( comm ) as DatabaseOneOffNotification < Config > ) ;
699702 }
700703
701704 async getOneOffNotifications (
702705 page : number ,
703706 pageSize : number ,
704707 ) : Promise < DatabaseOneOffNotification < Config > [ ] > {
705- const communications = await this . medplum . searchResources ( 'Communication' , {
706- _tag : 'notification,one-off' ,
707- _count : pageSize . toString ( ) ,
708- _offset : ( page * pageSize ) . toString ( ) ,
709- } ) ;
708+ const communications = await this . medplum . searchResources ( 'Communication' , [
709+ [ '_tag' , 'notification' ] ,
710+ [ '_tag' , 'one-off' ] ,
711+ [ '_count' , pageSize . toString ( ) ] ,
712+ [ '_offset' , ( page * pageSize ) . toString ( ) ] ,
713+ ] ) ;
710714 return communications . map ( ( comm ) => this . mapToDatabaseNotification ( comm ) as DatabaseOneOffNotification < Config > ) ;
711715 }
712716
@@ -745,16 +749,20 @@ export class MedplumNotificationBackend<Config extends BaseNotificationTypeConfi
745749 }
746750
747751 const searchParams = this . buildFhirSearchParams ( filter ) ;
748- this . ensureNotificationTag ( searchParams ) ;
749752 searchParams . _count = pageSize . toString ( ) ;
750753 searchParams . _offset = ( page * pageSize ) . toString ( ) ;
751754
752- const communications = await this . medplum . searchResources ( 'Communication' , searchParams ) ;
755+ // Convert to string[][] so that _tag values become repeated AND parameters
756+ // (comma-separated _tag in a single param means OR in FHIR, which is wrong here)
757+ const searchTuples = this . paramsToSearchTuples ( searchParams ) ;
758+
759+ const communications = await this . medplum . searchResources ( 'Communication' , searchTuples ) ;
753760 return communications . map ( ( comm ) => this . mapToDatabaseNotification ( comm ) ) ;
754761 }
755762
756763 /**
757764 * Ensure the `_tag` parameter always includes `notification`.
765+ * Mutates the params record in place.
758766 */
759767 private ensureNotificationTag ( params : Record < string , string > ) : void {
760768 if ( params . _tag ) {
@@ -766,6 +774,32 @@ export class MedplumNotificationBackend<Config extends BaseNotificationTypeConfi
766774 }
767775 }
768776
777+ /**
778+ * Convert a `Record<string, string>` FHIR search params object into `string[][]` tuples.
779+ *
780+ * This is needed because the `_tag` parameter uses comma-separated values internally
781+ * to accumulate multiple tags (notificationType + contextName + 'notification'), but
782+ * **FHIR treats comma-separated token values as OR**. To get AND semantics we must
783+ * repeat the parameter: `_tag=notification&_tag=SMS` instead of `_tag=notification,SMS`.
784+ *
785+ * All other parameters are passed through as single key-value pairs.
786+ */
787+ private paramsToSearchTuples ( params : Record < string , string > ) : string [ ] [ ] {
788+ this . ensureNotificationTag ( params ) ;
789+ const tuples : string [ ] [ ] = [ ] ;
790+ for ( const [ key , value ] of Object . entries ( params ) ) {
791+ if ( key === '_tag' ) {
792+ // Split into separate entries for AND semantics
793+ for ( const tag of value . split ( ',' ) ) {
794+ tuples . push ( [ '_tag' , tag . trim ( ) ] ) ;
795+ }
796+ } else {
797+ tuples . push ( [ key , value ] ) ;
798+ }
799+ }
800+ return tuples ;
801+ }
802+
769803 /**
770804 * Recursively build FHIR search parameters from a NotificationFilter tree.
771805 * Handles field filters, AND, and NOT. OR is handled at the caller level.
0 commit comments