Skip to main content

haste_server/fhir_client/middleware/operations/
mod.rs

1use crate::fhir_client::{
2    ClientState, ServerCTX,
3    middleware::{Backend, Handler, Output, operation_definitions},
4};
5use haste_fhir_client::request::{
6    FHIRInvokeInstanceResponse, FHIRInvokeSystemResponse, FHIRInvokeTypeResponse, FHIRRequest,
7    FHIRResponse, InvocationRequest, InvokeResponse,
8};
9use haste_fhir_model::r4::generated::{
10    resources::{OperationDefinition, Resource, ResourceType},
11    terminology::IssueType,
12};
13use haste_fhir_operation_error::OperationOutcomeError;
14use haste_fhir_ops::{OperationInvocation, defines};
15use haste_operation_executor::{
16    providers::deno_embedded::pool::DenoPool, traits::OperationExecutor,
17};
18use std::{borrow::Cow, sync::Arc};
19
20mod custom_operations;
21
22/// What a built-in operation runs with: the caller's context and the state
23/// of the pipeline that invoked it.
24pub struct ServerOperationContext<B: Backend> {
25    pub ctx: Arc<ServerCTX<B>>,
26    pub state: ClientState<B>,
27}
28
29type BuiltinOperation<B> = Box<dyn OperationInvocation<ServerOperationContext<B>>>;
30
31/// Ends the operation lane: runs the server's built-in operations, and a
32/// project's own OperationDefinitions through the Deno executor.
33pub struct Operations<B: Backend> {
34    builtins: Vec<BuiltinOperation<B>>,
35    deno_pool: Arc<DenoPool>,
36}
37
38impl<B: Backend> Operations<B> {
39    /// `deno_pool` should be a pool shared across all routes/clients derived
40    /// from the same server rather than a freshly-constructed one -- see
41    /// [`crate::fhir_client::ServerClientConfig::new`].
42    pub fn new(deno_pool: Arc<DenoPool>) -> Self {
43        let builtins: Vec<BuiltinOperation<B>> = vec![
44            Box::new(custom_operations::view_definition_run()),
45            Box::new(custom_operations::valueset_expand_op()),
46            Box::new(custom_operations::project_information_op()),
47            Box::new(custom_operations::active_refresh_tokens_op()),
48            Box::new(custom_operations::approved_scopes_op()),
49            Box::new(custom_operations::delete_approved_scope_op()),
50            Box::new(custom_operations::delete_refresh_token_op()),
51            Box::new(custom_operations::endpoint_metadata_op()),
52            Box::new(custom_operations::idp_registration_info_op()),
53            Box::new(custom_operations::evaluate_policy_op()),
54            Box::new(custom_operations::valueset_validate_code_op()),
55            Box::new(custom_operations::hl7v2_parse_op()),
56            Box::new(custom_operations::indexing_errors_op()),
57            Box::new(custom_operations::tenant_customization_op()),
58            Box::new(custom_operations::tenant_branding_op()),
59            Box::new(custom_operations::patient_everything()),
60            Box::new(custom_operations::smart_launch_op()),
61            Box::new(custom_operations::implementation_guide_install_op()),
62            Box::new(custom_operations::implementation_guide_uninstall_op()),
63        ];
64
65        Operations {
66            builtins,
67            deno_pool,
68        }
69    }
70
71    /// The OperationDefinitions of the operations implemented here, which
72    /// take precedence over a project's own.
73    pub fn operation_definitions(&self) -> operation_definitions::Builtins {
74        self.builtins
75            .iter()
76            .map(|operation| operation.operation_definition())
77            .collect()
78    }
79
80    /// The built-in operation whose definition defines `request`: its code,
81    /// at that level, on that resource type.
82    fn find_operation(
83        &self,
84        request: &InvocationRequest,
85    ) -> Option<&dyn OperationInvocation<ServerOperationContext<B>>> {
86        self.builtins
87            .iter()
88            .find(|operation| defines(operation.operation_definition(), request))
89            .map(|operation| operation.as_ref())
90    }
91}
92
93/// The project's (custom, tenant-authored) `OperationDefinition` that defines
94/// `request`, if there is one. The search lists built-ins too, but none
95/// defines the request or the caller would have run it instead.
96async fn find_custom_operation<B: Backend>(
97    ctx: &Arc<ServerCTX<B>>,
98    request: &InvocationRequest,
99) -> Result<Option<OperationDefinition>, OperationOutcomeError> {
100    let (code, level, _) = invocation(request);
101    let bundle = ctx
102        .client
103        .search_type(
104            ctx.clone(),
105            ResourceType::OperationDefinition,
106            vec![
107                ("code".to_string(), vec![code.to_string()]),
108                (level.to_string(), vec!["true".to_string()]),
109            ]
110            .into(),
111        )
112        .await?;
113
114    Ok(bundle
115        .entry
116        .as_deref()
117        .unwrap_or_default()
118        .iter()
119        .filter_map(|entry| match entry.resource.as_deref() {
120            Some(Resource::OperationDefinition(definition)) => Some(definition),
121            _ => None,
122        })
123        .find(|definition| defines(definition, request))
124        .cloned())
125}
126
127/// The OperationDefinition that defines `request`: a built-in's, or else the
128/// project's own.
129pub async fn operation_definition<B: Backend>(
130    ctx: &Arc<ServerCTX<B>>,
131    request: &InvocationRequest,
132) -> Result<Cow<'static, OperationDefinition>, OperationOutcomeError> {
133    let builtins = ctx.client.builtin_operations();
134    if let Some(builtin) = builtins.iter().find(|builtin| defines(builtin, request)) {
135        return Ok(Cow::Borrowed(*builtin));
136    }
137    find_custom_operation(ctx, request)
138        .await?
139        .map(Cow::Owned)
140        .ok_or_else(|| not_defined(request))
141}
142
143/// The error for an invocation that neither a built-in nor the project defines.
144fn not_defined(request: &InvocationRequest) -> OperationOutcomeError {
145    let (code, _, invoked) = invocation(request);
146    OperationOutcomeError::error(
147        IssueType::not_found(),
148        format!("No operation '{code}' is defined {invoked}."),
149    )
150}
151
152/// What `request` invokes: the code, the OperationDefinition search parameter
153/// for its level, and where, for an error message.
154fn invocation(request: &InvocationRequest) -> (&str, &'static str, String) {
155    match request {
156        InvocationRequest::Instance(request) => (
157            request.operation.name(),
158            "instance",
159            format!("on {} instances", request.resource_type.as_ref()),
160        ),
161        InvocationRequest::Type(request) => (
162            request.operation.name(),
163            "type",
164            format!("on the {} type", request.resource_type.as_ref()),
165        ),
166        InvocationRequest::System(request) => (
167            request.operation.name(),
168            "system",
169            "at the system level".to_string(),
170        ),
171    }
172}
173
174/// The response to `request`, at the level it was invoked at. The typed client
175/// methods (`invoke_instance`, `invoke_type`) only accept their own level.
176fn invoke_response(request: &InvocationRequest, resource: Resource) -> FHIRResponse {
177    FHIRResponse::Invoke(match request {
178        InvocationRequest::Instance(_) => {
179            InvokeResponse::Instance(FHIRInvokeInstanceResponse { resource })
180        }
181        InvocationRequest::Type(_) => InvokeResponse::Type(FHIRInvokeTypeResponse { resource }),
182        InvocationRequest::System(_) => {
183            InvokeResponse::System(FHIRInvokeSystemResponse { resource })
184        }
185    })
186}
187
188impl<B: Backend> Handler<B> for Operations<B> {
189    fn handle<'a>(
190        &'a self,
191        state: &'a ClientState<B>,
192        ctx: Arc<ServerCTX<B>>,
193        request: FHIRRequest,
194    ) -> Output<'a> {
195        Box::pin(async move {
196            let FHIRRequest::Invocation(invoke_request) = &request else {
197                return Err(OperationOutcomeError::fatal(
198                    IssueType::exception(),
199                    "Operation not supported".to_string(),
200                ));
201            };
202
203            let builtin = self.find_operation(invoke_request);
204            let custom = match builtin {
205                Some(_) => None,
206                None => Some(
207                    find_custom_operation(&ctx, invoke_request)
208                        .await?
209                        .ok_or_else(|| not_defined(invoke_request))?,
210                ),
211            };
212
213            // `Prefer: respond-async`: the operation exists and the request
214            // has passed every check before this point, so queue it and
215            // answer 202. The worker runs it through this same pipeline later.
216            if ctx.prefer_async {
217                return crate::jobs::operation::submit(
218                    state.repo.as_ref(),
219                    state.config.as_ref(),
220                    ctx.as_ref(),
221                    invoke_request,
222                )
223                .await;
224            }
225
226            if let Some(operation) = builtin {
227                let output = operation
228                    .execute(
229                        ServerOperationContext {
230                            state: state.clone(),
231                            ctx: ctx.clone(),
232                        },
233                        ctx.tenant.clone(),
234                        ctx.project.clone(),
235                        invoke_request,
236                    )
237                    .await?;
238
239                return Ok(invoke_response(invoke_request, output));
240            }
241
242            let operation_definition = custom.ok_or_else(|| not_defined(invoke_request))?;
243            let output = self
244                .deno_pool
245                .execute_operation(
246                    ctx.clone(),
247                    ctx.client.clone(),
248                    &operation_definition,
249                    invoke_request,
250                )
251                .await?;
252
253            Ok(invoke_response(
254                invoke_request,
255                Resource::Parameters(output),
256            ))
257        })
258    }
259}