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 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 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}