Skip to main content

haste_server/fhir_client/
mod.rs

1use crate::{
2    ServerEnvironmentVariables,
3    fhir_client::{
4        middleware::{
5            ServerMiddlewareContext, ServerMiddlewareNext, ServerMiddlewareOutput,
6            ServerMiddlewareState,
7        },
8        utilities::request_to_resource_type,
9    },
10};
11use haste_config::Config;
12use haste_fhir_client::{
13    FHIRClient,
14    middleware::{Middleware, MiddlewareChain},
15    request::{
16        FHIRBatchRequest, FHIRConditionalUpdateRequest, FHIRCreateRequest, FHIRReadRequest,
17        FHIRRequest, FHIRResponse, FHIRSearchTypeRequest, FHIRTransactionRequest,
18        FHIRUpdateInstanceRequest, SearchRequest, SearchResponse, UpdateRequest,
19    },
20    url::ParsedParameters,
21};
22use haste_fhir_model::r4::generated::resources::{
23    Bundle, CapabilityStatement, Parameters, Resource, ResourceType,
24};
25use haste_fhir_operation_error::{OperationOutcomeError, derive::OperationOutcomeError};
26use haste_fhir_search::SearchEngine;
27use haste_fhir_terminology::FHIRTerminology;
28use haste_jwt::{
29    AuthorId, AuthorKind, ProjectId, TenantId, UserRole,
30    claims::SubscriptionTier,
31    scopes::{
32        SMARTResourceScope, Scope, Scopes, SmartResourceScopeLevel, SmartResourceScopePermission,
33        SmartResourceScopePermissions, SmartResourceScopeUser, SmartScope,
34    },
35};
36use haste_repository::{Repository, types::SupportedFHIRVersions};
37use std::sync::{Arc, LazyLock};
38
39mod batch_transaction_processing;
40mod compartment;
41mod middleware;
42mod utilities;
43
44#[derive(OperationOutcomeError, Debug)]
45pub enum StorageError {
46    #[error(
47        code = "not-supported",
48        diagnostic = "Storage not supported for fhir method."
49    )]
50    NotSupported,
51    #[error(
52        code = "exception",
53        diagnostic = "No response was returned from the request."
54    )]
55    NoResponse,
56    #[error(
57        code = "not-found",
58        diagnostic = "Resource '{arg0:?}' with id '{arg1}' not found."
59    )]
60    NotFound(ResourceType, String),
61    #[error(code = "invalid", diagnostic = "Invalid resource type.")]
62    InvalidType,
63}
64
65pub struct ServerCTX<
66    Repo: Repository + Send + Sync + 'static,
67    Search: SearchEngine + Send + Sync + 'static,
68    Terminology: FHIRTerminology + Send + Sync + 'static,
69> {
70    pub tenant: TenantId,
71    pub project: ProjectId,
72    pub fhir_version: SupportedFHIRVersions,
73    pub user: Arc<haste_jwt::claims::UserTokenClaims>,
74    pub client: Arc<FHIRServerClient<Repo, Search, Terminology>>,
75    pub rate_limit: Arc<dyn haste_rate_limit::RateLimit>,
76}
77
78impl<
79    Repo: Repository + Send + Sync + 'static,
80    Search: SearchEngine + Send + Sync + 'static,
81    Terminology: FHIRTerminology + Send + Sync + 'static,
82> ServerCTX<Repo, Search, Terminology>
83{
84    pub fn new(
85        tenant: TenantId,
86        project: ProjectId,
87        fhir_version: SupportedFHIRVersions,
88        user: Arc<haste_jwt::claims::UserTokenClaims>,
89        client: Arc<FHIRServerClient<Repo, Search, Terminology>>,
90        rate_limit: Arc<dyn haste_rate_limit::RateLimit>,
91    ) -> Self {
92        ServerCTX {
93            tenant,
94            project,
95            fhir_version,
96            user,
97            client,
98            rate_limit,
99        }
100    }
101
102    pub fn system(
103        tenant: TenantId,
104        project: ProjectId,
105        client: Arc<FHIRServerClient<Repo, Search, Terminology>>,
106        rate_limit: Arc<dyn haste_rate_limit::RateLimit>,
107    ) -> Self {
108        ServerCTX {
109            tenant: tenant.clone(),
110            project: project.clone(),
111            fhir_version: SupportedFHIRVersions::R4,
112            user: Arc::new(haste_jwt::claims::UserTokenClaims {
113                sub: AuthorId::System,
114                exp: 0,
115                aud: AuthorKind::System.to_string(),
116                user_role: UserRole::Owner,
117                project: Some(project),
118                tenant,
119                subscription_tier: SubscriptionTier::Unlimited,
120                scope: Scopes(vec![Scope::SMART(SmartScope::Resource(
121                    SMARTResourceScope {
122                        user: SmartResourceScopeUser::System,
123                        level: SmartResourceScopeLevel::AllResources,
124                        permissions: SmartResourceScopePermissions::new(vec![
125                            SmartResourceScopePermission::Create,
126                            SmartResourceScopePermission::Read,
127                            SmartResourceScopePermission::Update,
128                            SmartResourceScopePermission::Delete,
129                            SmartResourceScopePermission::Search,
130                        ]),
131                    },
132                ))]),
133                user_id: AuthorId::System,
134                resource_type: AuthorKind::System,
135                access_policy_version_ids: vec![],
136                membership: None,
137            }),
138            client,
139            rate_limit,
140        }
141    }
142}
143
144struct ClientState<
145    Repo: Repository + Send + Sync,
146    Search: SearchEngine + Send + Sync,
147    Terminology: FHIRTerminology + Send + Sync,
148> {
149    repo: Arc<Repo>,
150    search: Arc<Search>,
151    terminology: Arc<Terminology>,
152    config: Arc<dyn Config<ServerEnvironmentVariables>>,
153}
154
155pub struct Route<
156    Repo: Repository + Send + Sync + 'static,
157    Search: SearchEngine + Send + Sync + 'static,
158    Terminology: FHIRTerminology + Send + Sync + 'static,
159> {
160    filter: Box<dyn Fn(&FHIRRequest) -> bool + Send + Sync>,
161    middleware: Middleware<
162        Arc<ClientState<Repo, Search, Terminology>>,
163        Arc<ServerCTX<Repo, Search, Terminology>>,
164        FHIRRequest,
165        FHIRResponse,
166        OperationOutcomeError,
167    >,
168}
169
170pub struct FHIRServerClient<
171    Repo: Repository + Send + Sync + 'static,
172    Search: SearchEngine + Send + Sync + 'static,
173    Terminology: FHIRTerminology + Send + Sync + 'static,
174> {
175    state: Arc<ClientState<Repo, Search, Terminology>>,
176    middleware: Middleware<
177        Arc<ClientState<Repo, Search, Terminology>>,
178        Arc<ServerCTX<Repo, Search, Terminology>>,
179        FHIRRequest,
180        FHIRResponse,
181        OperationOutcomeError,
182    >,
183}
184
185pub struct RouterMiddleware<
186    Repo: Repository + Send + Sync + 'static,
187    Search: SearchEngine + Send + Sync + 'static,
188    Terminology: FHIRTerminology + Send + Sync + 'static,
189> {
190    routes: Arc<Vec<Route<Repo, Search, Terminology>>>,
191}
192
193impl<
194    Repo: Repository + Send + Sync + 'static,
195    Search: SearchEngine + Send + Sync + 'static,
196    Terminology: FHIRTerminology + Send + Sync + 'static,
197> RouterMiddleware<Repo, Search, Terminology>
198{
199    pub fn new(routes: Arc<Vec<Route<Repo, Search, Terminology>>>) -> Self {
200        RouterMiddleware { routes }
201    }
202}
203
204impl<
205    Repo: Repository + Send + Sync + 'static,
206    Search: SearchEngine + Send + Sync + 'static,
207    Terminology: FHIRTerminology + Send + Sync + 'static,
208>
209    MiddlewareChain<
210        ServerMiddlewareState<Repo, Search, Terminology>,
211        Arc<ServerCTX<Repo, Search, Terminology>>,
212        FHIRRequest,
213        FHIRResponse,
214        OperationOutcomeError,
215    > for RouterMiddleware<Repo, Search, Terminology>
216{
217    fn call(
218        &self,
219        state: ServerMiddlewareState<Repo, Search, Terminology>,
220        context: ServerMiddlewareContext<Repo, Search, Terminology>,
221        next: Option<Arc<ServerMiddlewareNext<Repo, Search, Terminology>>>,
222    ) -> ServerMiddlewareOutput<Repo, Search, Terminology> {
223        let routes = self.routes.clone();
224        Box::pin(async move {
225            let route = routes.iter().find(|r| (r.filter)(&context.request));
226            match route {
227                Some(route) => {
228                    let context = route
229                        .middleware
230                        .call(state.clone(), context.ctx, context.request)
231                        .await?;
232                    if let Some(next) = next {
233                        next(state, context).await
234                    } else {
235                        Ok(context)
236                    }
237                }
238                None => {
239                    if let Some(next) = next {
240                        next(state, context).await
241                    } else {
242                        Ok(context)
243                    }
244                }
245            }
246        })
247    }
248}
249
250static ARTIFACT_TYPES: &[ResourceType] = &[
251    ResourceType::ValueSet,
252    ResourceType::CodeSystem,
253    ResourceType::StructureDefinition,
254    ResourceType::SearchParameter,
255];
256
257static TENANT_AUTH_TYPES: &[ResourceType] = &[
258    ResourceType::User,
259    ResourceType::Project,
260    ResourceType::IdentityProvider,
261];
262static PROJECT_AUTH_TYPES: &[ResourceType] = &[ResourceType::Membership];
263
264static SPECIAL_TYPES: LazyLock<Vec<ResourceType>> = LazyLock::new(|| {
265    [
266        &TENANT_AUTH_TYPES[..],
267        &PROJECT_AUTH_TYPES[..],
268        &ARTIFACT_TYPES[..],
269    ]
270    .concat()
271});
272
273pub struct ServerClientConfig<
274    Repo: Repository + Send + Sync + 'static,
275    Search: SearchEngine + Send + Sync + 'static,
276    Terminology: FHIRTerminology + Send + Sync + 'static,
277> {
278    pub repo: Arc<Repo>,
279    pub search: Arc<Search>,
280    pub terminology: Arc<Terminology>,
281    pub mutate_artifacts: bool,
282    pub config: Arc<dyn Config<ServerEnvironmentVariables>>,
283}
284
285impl<
286    Repo: Repository + Send + Sync + 'static,
287    Search: SearchEngine + Send + Sync + 'static,
288    Terminology: FHIRTerminology + Send + Sync + 'static,
289> ServerClientConfig<Repo, Search, Terminology>
290{
291    pub fn new(
292        repo: Arc<Repo>,
293        search: Arc<Search>,
294        terminology: Arc<Terminology>,
295        config: Arc<dyn Config<ServerEnvironmentVariables>>,
296    ) -> Self {
297        ServerClientConfig {
298            repo,
299            search,
300            terminology,
301            mutate_artifacts: false,
302            config,
303        }
304    }
305
306    pub fn allow_mutate_artifacts(
307        repo: Arc<Repo>,
308        search: Arc<Search>,
309        terminology: Arc<Terminology>,
310        config: Arc<dyn Config<ServerEnvironmentVariables>>,
311    ) -> Self {
312        Self {
313            repo,
314            search,
315            terminology,
316            config,
317            mutate_artifacts: true,
318        }
319    }
320}
321
322impl<
323    Repo: Repository + Send + Sync + 'static,
324    Search: SearchEngine + Send + Sync + 'static,
325    Terminology: FHIRTerminology + Send + Sync + 'static,
326> FHIRServerClient<Repo, Search, Terminology>
327{
328    pub fn new(config: ServerClientConfig<Repo, Search, Terminology>) -> Self {
329        let clinical_resources_route = Route {
330            filter: Box::new(|req: &FHIRRequest| match req {
331                FHIRRequest::Invocation(_) | FHIRRequest::Capabilities => false,
332                _ => {
333                    if let Some(resource_type) = request_to_resource_type(req) {
334                        !SPECIAL_TYPES.contains(&resource_type)
335                    } else {
336                        true
337                    }
338                }
339            }),
340            middleware: Middleware::new(vec![Box::new(middleware::storage::Middleware::new())]),
341        };
342
343        let operation_invocation_routes = Route {
344            filter: Box::new(|req: &FHIRRequest| match req {
345                FHIRRequest::Invocation(_) => true,
346                _ => false,
347            }),
348            middleware: Middleware::new(vec![Box::new(middleware::operations::Middleware::new())]),
349        };
350
351        let artifact_routes = Route {
352            filter: if config.mutate_artifacts {
353                Box::new(|req: &FHIRRequest| match req {
354                    FHIRRequest::Update(_)
355                    | FHIRRequest::Read(_)
356                    | FHIRRequest::Search(SearchRequest::Type(_)) => {
357                        if let Some(resource_type) = request_to_resource_type(req) {
358                            ARTIFACT_TYPES.contains(&resource_type)
359                        } else {
360                            false
361                        }
362                    }
363                    _ => false,
364                })
365            } else {
366                Box::new(|req: &FHIRRequest| match req {
367                    FHIRRequest::Read(_) | FHIRRequest::Search(SearchRequest::Type(_)) => {
368                        if let Some(resource_type) = request_to_resource_type(req) {
369                            ARTIFACT_TYPES.contains(&resource_type)
370                        } else {
371                            false
372                        }
373                    }
374                    _ => false,
375                })
376            },
377            middleware: Middleware::new(vec![
378                Box::new(middleware::set_artifact_tenant::Middleware::new()),
379                Box::new(middleware::storage::Middleware::new()),
380            ]),
381        };
382
383        let project_auth_routes = Route {
384            filter: Box::new(|req: &FHIRRequest| match req {
385                FHIRRequest::Invocation(_) => false,
386                _ => request_to_resource_type(req)
387                    .map_or(false, |rt| PROJECT_AUTH_TYPES.contains(rt)),
388            }),
389            middleware: Middleware::new(vec![
390                Box::new(middleware::transaction::Middleware::new()),
391                Box::new(middleware::custom_models::membership::Middleware::new()),
392                Box::new(middleware::storage::Middleware::new()),
393            ]),
394        };
395
396        let tenant_auth_routes = Route {
397            filter: Box::new(|req: &FHIRRequest| match req {
398                FHIRRequest::Invocation(_) => false,
399                _ => {
400                    request_to_resource_type(req).map_or(false, |rt| TENANT_AUTH_TYPES.contains(rt))
401                }
402            }),
403            middleware: Middleware::new(vec![
404                Box::new(
405                    middleware::check_project::SetProjectReadOnlyMiddleware::new(ProjectId::System),
406                ),
407                // Confirm in system project as above will only set to system if readonly.
408                Box::new(middleware::check_project::Middleware::new(
409                    ProjectId::System,
410                )),
411                Box::new(middleware::transaction::Middleware::new()),
412                Box::new(middleware::custom_models::project::Middleware::new()),
413                Box::new(middleware::custom_models::user::Middleware::new()),
414                Box::new(middleware::storage::Middleware::new()),
415            ]),
416        };
417
418        let route_middleware = RouterMiddleware::new(Arc::new(vec![
419            clinical_resources_route,
420            artifact_routes,
421            operation_invocation_routes,
422            // Special Authentication routes.
423            project_auth_routes,
424            tenant_auth_routes,
425        ]));
426
427        FHIRServerClient {
428            state: Arc::new(ClientState {
429                repo: config.repo,
430                search: config.search,
431                terminology: config.terminology,
432                config: config.config,
433            }),
434            middleware: Middleware::new(vec![
435                Box::new(middleware::rate_limit::Middleware::new()),
436                Box::new(middleware::auth_z::scope_check::SMARTScopeAccessMiddleware::new()),
437                Box::new(middleware::auth_z::access_control::AccessControlMiddleware::new()),
438                Box::new(route_middleware),
439                Box::new(middleware::capabilities::Middleware::new()),
440            ]),
441        }
442    }
443}
444
445impl<
446    Repo: Repository + Send + Sync + 'static,
447    Search: SearchEngine + Send + Sync + 'static,
448    Terminology: FHIRTerminology + Send + Sync + 'static,
449> FHIRClient<Arc<ServerCTX<Repo, Search, Terminology>>, OperationOutcomeError>
450    for FHIRServerClient<Repo, Search, Terminology>
451{
452    async fn request(
453        &self,
454        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
455        request: FHIRRequest,
456    ) -> Result<FHIRResponse, OperationOutcomeError> {
457        let response = self
458            .middleware
459            .call(self.state.clone(), _ctx, request)
460            .await?;
461
462        response
463            .response
464            .ok_or_else(|| StorageError::NoResponse.into())
465    }
466
467    async fn capabilities(
468        &self,
469        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
470    ) -> Result<CapabilityStatement, OperationOutcomeError> {
471        let res = self
472            .middleware
473            .call(self.state.clone(), _ctx, FHIRRequest::Capabilities)
474            .await?;
475
476        match res.response {
477            Some(FHIRResponse::Capabilities(capabilities_response)) => {
478                Ok(capabilities_response.capabilities)
479            }
480            _ => panic!("Unexpected response type"),
481        }
482    }
483
484    async fn search_system(
485        &self,
486        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
487        _parameters: ParsedParameters,
488    ) -> Result<Bundle, OperationOutcomeError> {
489        todo!()
490    }
491
492    async fn search_type(
493        &self,
494        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
495        resource_type: ResourceType,
496        parameters: ParsedParameters,
497    ) -> Result<Bundle, OperationOutcomeError> {
498        let res = self
499            .middleware
500            .call(
501                self.state.clone(),
502                ctx,
503                FHIRRequest::Search(SearchRequest::Type(FHIRSearchTypeRequest {
504                    resource_type,
505                    parameters,
506                })),
507            )
508            .await?;
509
510        match res.response {
511            Some(FHIRResponse::Search(SearchResponse::Type(search_response))) => {
512                Ok(search_response.bundle)
513            }
514            _ => panic!("Unexpected response type"),
515        }
516    }
517
518    async fn create(
519        &self,
520        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
521        resource_type: ResourceType,
522        resource: Resource,
523    ) -> Result<Resource, OperationOutcomeError> {
524        let res = self
525            .middleware
526            .call(
527                self.state.clone(),
528                ctx,
529                FHIRRequest::Create(FHIRCreateRequest {
530                    resource_type,
531                    resource,
532                }),
533            )
534            .await?;
535
536        match res.response {
537            Some(FHIRResponse::Create(create_response)) => Ok(create_response.resource),
538            _ => panic!("Unexpected response type"),
539        }
540    }
541
542    async fn update(
543        &self,
544        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
545        resource_type: ResourceType,
546        id: String,
547        resource: Resource,
548    ) -> Result<Resource, OperationOutcomeError> {
549        let res = self
550            .middleware
551            .call(
552                self.state.clone(),
553                ctx,
554                FHIRRequest::Update(UpdateRequest::Instance(FHIRUpdateInstanceRequest {
555                    resource_type,
556                    id,
557                    resource,
558                })),
559            )
560            .await?;
561
562        match res.response {
563            Some(FHIRResponse::Create(create_response)) => Ok(create_response.resource),
564            Some(FHIRResponse::Update(update_response)) => Ok(update_response.resource),
565            _ => panic!("Unexpected response type {:?}", res.response),
566        }
567    }
568
569    async fn conditional_update(
570        &self,
571        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
572        resource_type: ResourceType,
573        parameters: ParsedParameters,
574        resource: Resource,
575    ) -> Result<Resource, OperationOutcomeError> {
576        let res = self
577            .middleware
578            .call(
579                self.state.clone(),
580                ctx,
581                FHIRRequest::Update(UpdateRequest::Conditional(FHIRConditionalUpdateRequest {
582                    resource_type,
583                    parameters,
584                    resource,
585                })),
586            )
587            .await?;
588
589        match res.response {
590            Some(FHIRResponse::Create(create_response)) => Ok(create_response.resource),
591            Some(FHIRResponse::Update(update_response)) => Ok(update_response.resource),
592            _ => panic!("Unexpected response type {:?}", res.response),
593        }
594    }
595
596    async fn patch(
597        &self,
598        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
599        _resource_type: ResourceType,
600        _id: String,
601        _patches: json_patch::Patch,
602    ) -> Result<Resource, OperationOutcomeError> {
603        todo!()
604    }
605
606    async fn read(
607        &self,
608        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
609        resource_type: ResourceType,
610        id: String,
611    ) -> Result<Option<Resource>, OperationOutcomeError> {
612        let res = self
613            .middleware
614            .call(
615                self.state.clone(),
616                ctx,
617                FHIRRequest::Read(FHIRReadRequest { resource_type, id }),
618            )
619            .await?;
620
621        match res.response {
622            Some(FHIRResponse::Read(read_response)) => Ok(read_response.resource),
623            _ => panic!("Unexpected response type"),
624        }
625    }
626
627    async fn vread(
628        &self,
629        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
630        _resource_type: ResourceType,
631        _id: String,
632        _version_id: String,
633    ) -> Result<Option<Resource>, OperationOutcomeError> {
634        todo!()
635    }
636
637    async fn delete_instance(
638        &self,
639        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
640        _resource_type: ResourceType,
641        _id: String,
642    ) -> Result<(), OperationOutcomeError> {
643        todo!()
644    }
645
646    async fn delete_type(
647        &self,
648        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
649        _resource_type: ResourceType,
650        _parameters: ParsedParameters,
651    ) -> Result<(), OperationOutcomeError> {
652        todo!()
653    }
654
655    async fn delete_system(
656        &self,
657        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
658        _parameters: ParsedParameters,
659    ) -> Result<(), OperationOutcomeError> {
660        todo!()
661    }
662
663    async fn history_system(
664        &self,
665        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
666        _parameters: ParsedParameters,
667    ) -> Result<Bundle, OperationOutcomeError> {
668        todo!()
669    }
670
671    async fn history_type(
672        &self,
673        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
674        _resource_type: ResourceType,
675        _parameters: ParsedParameters,
676    ) -> Result<Bundle, OperationOutcomeError> {
677        todo!()
678    }
679
680    async fn history_instance(
681        &self,
682        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
683        _resource_type: ResourceType,
684        _id: String,
685        _parameters: ParsedParameters,
686    ) -> Result<Bundle, OperationOutcomeError> {
687        todo!()
688    }
689
690    async fn invoke_instance(
691        &self,
692        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
693        _resource_type: ResourceType,
694        _id: String,
695        _operation: String,
696        _parameters: Parameters,
697    ) -> Result<Resource, OperationOutcomeError> {
698        todo!()
699    }
700
701    async fn invoke_type(
702        &self,
703        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
704        _resource_type: ResourceType,
705        _operation: String,
706        _parameters: Parameters,
707    ) -> Result<Resource, OperationOutcomeError> {
708        todo!()
709    }
710
711    async fn invoke_system(
712        &self,
713        _ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
714        _operation: String,
715        _parameters: Parameters,
716    ) -> Result<Resource, OperationOutcomeError> {
717        todo!()
718    }
719
720    async fn transaction(
721        &self,
722        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
723        bundle: Bundle,
724    ) -> Result<Bundle, OperationOutcomeError> {
725        let res = self
726            .middleware
727            .call(
728                self.state.clone(),
729                ctx,
730                FHIRRequest::Transaction(FHIRTransactionRequest { resource: bundle }),
731            )
732            .await?;
733
734        match res.response {
735            Some(FHIRResponse::Transaction(transaction_response)) => {
736                Ok(transaction_response.resource)
737            }
738            _ => panic!("Unexpected response type"),
739        }
740    }
741
742    async fn batch(
743        &self,
744        ctx: Arc<ServerCTX<Repo, Search, Terminology>>,
745        bundle: Bundle,
746    ) -> Result<Bundle, OperationOutcomeError> {
747        let res = self
748            .middleware
749            .call(
750                self.state.clone(),
751                ctx,
752                FHIRRequest::Batch(FHIRBatchRequest { resource: bundle }),
753            )
754            .await?;
755
756        match res.response {
757            Some(FHIRResponse::Batch(batch_response)) => Ok(batch_response.resource),
758            _ => panic!("Unexpected response type"),
759        }
760    }
761}