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