Skip to main content

haste_repository/pg/models/
fhir.rs

1use crate::{
2    fhir::{CachePolicy, FHIRRepository, ResourceHistoryValue},
3    pg::{
4        PGConnection, StoreError,
5        pending::{self, PendingRows},
6        transaction::{commit_transaction, create_transaction},
7    },
8    types::{FHIRMethod, SupportedFHIRVersions},
9    utilities,
10};
11use haste_fhir_client::{
12    request::HistoryRequest,
13    url::{ParsedParameter, ParsedParameters},
14};
15use haste_fhir_model::r4::{
16    datetime::parse_datetime,
17    generated::{
18        resources::{Resource, ResourceType},
19        terminology::IssueType,
20    },
21    sqlx::FHIRJson,
22};
23use haste_fhir_operation_error::OperationOutcomeError;
24use haste_jwt::{ProjectId, ResourceId, TenantId, VersionId, claims::UserTokenClaims};
25use moka::future::Cache;
26use sqlx::{PgExecutor, Postgres, QueryBuilder, Row, query_builder::Separated};
27use std::{collections::HashMap, sync::Arc};
28use tokio::sync::Mutex;
29
30#[derive(Debug)]
31struct ReturnVersionedResource {
32    resource: Resource,
33    version_id: VersionId,
34}
35
36#[derive(sqlx::FromRow, Debug)]
37struct HistoryValue {
38    pub resource: FHIRJson<Resource>,
39    pub request_method: String,
40}
41
42async fn read_version_ids_from_cache<'a>(
43    cache: &Cache<VersionId, Resource>,
44    version_ids: &'a [&VersionId],
45) -> (Vec<Resource>, Vec<&'a VersionId>) {
46    let mut remaining_version_ids = vec![];
47    let mut cached_resources = vec![];
48    for version_id in version_ids {
49        if let Some(resource) = cache.get(*version_id).await {
50            cached_resources.push(resource);
51        } else {
52            remaining_version_ids.push(*version_id);
53        }
54    }
55
56    (cached_resources, remaining_version_ids)
57}
58
59impl FHIRRepository for PGConnection {
60    async fn create(
61        &self,
62        tenant: &TenantId,
63        project: &ProjectId,
64        author: &UserTokenClaims,
65        fhir_version: &SupportedFHIRVersions,
66        mut resource: Resource,
67    ) -> Result<Resource, OperationOutcomeError> {
68        utilities::set_resource_id(&mut resource, None)?;
69        utilities::set_resource_meta(&mut resource, &author.resource_type, &author.sub)?;
70        write_resource(
71            self,
72            tenant,
73            project,
74            author,
75            fhir_version,
76            resource,
77            false,
78            "POST",
79            FHIRMethod::Create,
80        )
81        .await
82    }
83
84    async fn delete(
85        &self,
86        tenant: &TenantId,
87        project: &ProjectId,
88        author: &UserTokenClaims,
89        fhir_version: &SupportedFHIRVersions,
90        mut resource: Resource,
91        id: &str,
92    ) -> Result<Resource, OperationOutcomeError> {
93        utilities::set_resource_id(&mut resource, Some(id.to_string()))?;
94        utilities::set_resource_meta(&mut resource, &author.resource_type, &author.sub)?;
95        write_resource(
96            self,
97            tenant,
98            project,
99            author,
100            fhir_version,
101            resource,
102            true,
103            "DELETE",
104            FHIRMethod::Delete,
105        )
106        .await
107    }
108
109    async fn update(
110        &self,
111        tenant: &TenantId,
112        project: &ProjectId,
113        author: &UserTokenClaims,
114        fhir_version: &SupportedFHIRVersions,
115        mut resource: Resource,
116        id: &str,
117    ) -> Result<Resource, OperationOutcomeError> {
118        utilities::set_resource_id(&mut resource, Some(id.to_string()))?;
119        utilities::set_resource_meta(&mut resource, &author.resource_type, &author.sub)?;
120        write_resource(
121            self,
122            tenant,
123            project,
124            author,
125            fhir_version,
126            resource,
127            false,
128            "PUT",
129            FHIRMethod::Update,
130        )
131        .await
132    }
133
134    async fn read_by_version_ids(
135        &self,
136        tenant_id: &TenantId,
137        project_id: &ProjectId,
138        version_ids: &[&VersionId],
139        cache_policy: CachePolicy,
140    ) -> Result<Vec<Resource>, OperationOutcomeError> {
141        if version_ids.is_empty() {
142            return Ok(Vec::new());
143        }
144
145        let (cached_result, remaining_version_ids) =
146            read_version_ids_from_cache(self.cache(), version_ids).await;
147
148        if remaining_version_ids.is_empty() {
149            return Ok(cached_result);
150        }
151
152        match self {
153            PGConnection::Pool(pool, cache) => {
154                let res = read_by_version_ids(pool, tenant_id, project_id, &remaining_version_ids)
155                    .await?;
156
157                if cache_policy == CachePolicy::Cache {
158                    for v in &res {
159                        cache.insert(v.version_id.clone(), v.resource.clone()).await;
160                    }
161                }
162
163                Ok(cached_result
164                    .into_iter()
165                    .chain(res.into_iter().map(|r| r.resource))
166                    .collect::<Vec<_>>())
167            }
168            PGConnection::Transaction(tx, cache, pending) => {
169                pending.flush(tx).await?;
170                let mut conn = tx.lock().await;
171                // Handle PgConnection connection
172                let res =
173                    read_by_version_ids(&mut **conn, tenant_id, project_id, &remaining_version_ids)
174                        .await?;
175
176                if cache_policy == CachePolicy::Cache {
177                    for v in &res {
178                        cache.insert(v.version_id.clone(), v.resource.clone()).await;
179                    }
180                }
181
182                Ok(cached_result
183                    .into_iter()
184                    .chain(res.into_iter().map(|r| r.resource))
185                    .collect::<Vec<_>>())
186            }
187        }
188    }
189
190    async fn read_latest(
191        &self,
192        tenant_id: &TenantId,
193        project_id: &ProjectId,
194        resource_type: &ResourceType,
195        resource_id: &ResourceId,
196    ) -> Result<Option<Resource>, OperationOutcomeError> {
197        match self {
198            PGConnection::Pool(pool, _) => {
199                let res =
200                    read_latest(pool, tenant_id, project_id, resource_type, resource_id).await?;
201                Ok(res)
202            }
203            PGConnection::Transaction(tx, _, pending) => {
204                pending.flush(tx).await?;
205                let mut conn = tx.lock().await;
206                // Handle PgConnection connection
207                read_latest(
208                    &mut **conn,
209                    tenant_id,
210                    project_id,
211                    resource_type,
212                    resource_id,
213                )
214                .await
215            }
216        }
217    }
218
219    async fn history(
220        &self,
221        tenant_id: &TenantId,
222        project_id: &ProjectId,
223        request: &HistoryRequest,
224    ) -> Result<Vec<ResourceHistoryValue>, OperationOutcomeError> {
225        match self {
226            PGConnection::Pool(pool, _) => history(pool, tenant_id, project_id, request).await,
227            PGConnection::Transaction(tx, _, pending) => {
228                pending.flush(tx).await?;
229                let mut conn = tx.lock().await;
230                // Handle PgConnection connection
231                history(&mut **conn, tenant_id, project_id, request).await
232            }
233        }
234    }
235
236    fn in_transaction(&self) -> bool {
237        matches!(self, PGConnection::Transaction(_tx, _, _))
238    }
239
240    async fn transaction(&self, is_updating_sequence: bool) -> Result<Self, OperationOutcomeError> {
241        let tx = create_transaction(self, is_updating_sequence).await?;
242        let pending = match self {
243            PGConnection::Transaction(_, _, pending) => pending.clone(),
244            PGConnection::Pool(_, _) => PendingRows::new(),
245        };
246        Ok(PGConnection::Transaction(tx, self.cache().clone(), pending))
247    }
248
249    async fn commit(self) -> Result<(), OperationOutcomeError> {
250        match self {
251            PGConnection::Pool(_pool, _) => Err(StoreError::NotTransaction.into()),
252            PGConnection::Transaction(tx, _, pending) => {
253                pending.flush(&tx).await?;
254                commit_transaction(tx).await
255            }
256        }
257    }
258
259    async fn rollback(self) -> Result<(), OperationOutcomeError> {
260        match self {
261            PGConnection::Pool(_pool, _) => Err(StoreError::NotTransaction.into()),
262            PGConnection::Transaction(tx, _, _pending) => {
263                let conn = Mutex::into_inner(
264                    Arc::try_unwrap(tx).map_err(|_e| StoreError::FailedCommitTransaction)?,
265                );
266
267                // Handle PgConnection connection
268                conn.rollback().await.map_err(StoreError::from)?;
269                Ok(())
270            }
271        }
272    }
273}
274
275/// Inserts (Pool) or buffers (Transaction) a `resources` row for
276/// create/update/delete and hands the resource back to the caller. The
277/// `resource` argument must already be mutated (`id`/`meta` set) by the
278/// caller. A `Pool` write is never buffered, so it's inserted straight from
279/// a borrow of `resource` with no clone; a `Transaction` write is queued via
280/// `PendingRows::push`, which needs its own owned copy since it must outlive
281/// this call until flush/commit.
282#[allow(clippy::too_many_arguments)]
283async fn write_resource(
284    conn: &PGConnection,
285    tenant: &TenantId,
286    project: &ProjectId,
287    author: &UserTokenClaims,
288    fhir_version: &SupportedFHIRVersions,
289    resource: Resource,
290    deleted: bool,
291    request_method: &'static str,
292    fhir_method: FHIRMethod,
293) -> Result<Resource, OperationOutcomeError> {
294    match conn {
295        PGConnection::Pool(_pool, _) => {
296            let tx = create_transaction(conn, true).await?;
297            {
298                let mut c = tx.lock().await;
299                pending::execute(
300                    &mut **c,
301                    tenant,
302                    project,
303                    author,
304                    fhir_version,
305                    &resource,
306                    deleted,
307                    request_method,
308                    fhir_method,
309                )
310                .await?;
311            }
312            commit_transaction(tx).await?;
313        }
314        PGConnection::Transaction(_tx, _, pending) => {
315            pending
316                .push(
317                    tenant,
318                    project,
319                    author,
320                    fhir_version,
321                    resource.clone(),
322                    deleted,
323                    request_method,
324                    fhir_method,
325                )
326                .await;
327        }
328    }
329
330    Ok(resource)
331}
332
333async fn read_by_version_ids<'a, 'e, E>(
334    executor: E,
335    tenant_id: &'a TenantId,
336    project_id: &'a ProjectId,
337    version_ids: &'a Vec<&'a VersionId>,
338) -> Result<Vec<ReturnVersionedResource>, OperationOutcomeError>
339where
340    E: PgExecutor<'e>,
341{
342    let bound_version_ids: Vec<&str> = version_ids
343        .iter()
344        .map(std::convert::AsRef::as_ref)
345        .collect();
346
347    // Fetched as raw `PgRow`s (not `query_as` into a `FromRow` struct) so the
348    // JSON body can be decoded straight out of each row's own buffer.
349    let rows = sqlx::query(
350        r"
351            SELECT resource, resource_type, version_id
352            FROM resources
353            WHERE tenant = $1 AND project = $2 AND version_id = ANY($3::text[])
354        ",
355    )
356    .bind(tenant_id.as_ref())
357    .bind(project_id.as_ref())
358    .bind(&bound_version_ids)
359    .fetch_all(executor)
360    .await
361    .map_err(StoreError::from)?;
362
363    // Create a lookup table to remember the original order of the requested version IDs.
364    let mut requested_order: HashMap<&VersionId, usize> = HashMap::with_capacity(version_ids.len());
365    for (index, version_id) in version_ids.iter().enumerate() {
366        requested_order.insert(*version_id, index);
367    }
368
369    let mut response = rows
370        .iter()
371        .map(|row| {
372            let resource_type: ResourceType =
373                row.try_get("resource_type").map_err(StoreError::from)?;
374            let version_id: VersionId = row.try_get("version_id").map_err(StoreError::from)?;
375
376            // Borrow the JSONB payload directly out of the row and hand the
377            // raw bytes straight to `ResourceType::deserialize` (which
378            // dispatches to the concrete struct's `serde_json::from_slice`)
379            // Avoids enum allocation by deserializing directly from the raw bytes.
380            let raw = row.try_get_raw("resource").map_err(StoreError::from)?;
381            let bytes = raw
382                .as_bytes()
383                .map_err(|e| StoreError::DeserializeError(e.to_string()))?;
384            // Strip the leading JSONB binary version-marker byte.
385            let resource = resource_type
386                .deserialize(&bytes[1..])
387                .map_err(|e| StoreError::DeserializeError(e.to_string()))?;
388
389            Ok(ReturnVersionedResource {
390                resource,
391                version_id,
392            })
393        })
394        .collect::<Result<Vec<_>, StoreError>>()?;
395
396    response.sort_by_key(|r| {
397        requested_order
398            .get(&r.version_id)
399            .copied()
400            .unwrap_or(usize::MAX)
401    });
402
403    Ok(response)
404}
405
406async fn read_latest<'a, 'e, E>(
407    executor: E,
408    tenant_id: &'a TenantId,
409    project_id: &'a ProjectId,
410    resource_type: &'a ResourceType,
411    resource_id: &'a ResourceId,
412) -> Result<Option<Resource>, OperationOutcomeError>
413where
414    E: PgExecutor<'e>,
415{
416    let response = sqlx::query_as::<_, (FHIRJson<Resource>, bool)>(
417        r"
418            SELECT resource, deleted
419            FROM resources
420            WHERE tenant = $1 AND project = $2 AND id = $3 AND resource_type = $4
421            ORDER BY sequence DESC
422            LIMIT 1
423        ",
424    )
425    .bind(tenant_id.as_ref())
426    .bind(project_id.as_ref())
427    .bind(resource_id.as_ref())
428    .bind(resource_type.as_ref())
429    .fetch_optional(executor)
430    .await
431    .map_err(StoreError::from)?;
432
433    match response {
434        Some((_, true)) | None => Ok(None),
435        Some((json, _)) => Ok(Some(json.0)),
436    }
437}
438
439fn process_history_parameters<'a>(
440    parameters: &'a ParsedParameters,
441    clauses: &mut Separated<'_, 'a, Postgres, &str>,
442) -> Result<(), OperationOutcomeError> {
443    for parameter in parameters.parameters() {
444        match parameter {
445            ParsedParameter::Result(result_param) => {
446                if result_param.name.as_str() == "_since" {
447                    if let Some(value) = result_param.value.first() {
448                        let date_time = parse_datetime(value.as_str()).map_err(|e| {
449                            OperationOutcomeError::fatal(
450                                IssueType::invalid(),
451                                format!("Invalid _since parameter datetime: {e:?}"),
452                            )
453                        })?;
454
455                        clauses.push(" created_at >= ").push_bind_unseparated(
456                            chrono::DateTime::try_from(date_time).map_err(|e| {
457                                OperationOutcomeError::fatal(
458                                    IssueType::invalid(),
459                                    format!("Invalid _since parameter datetime: {e:?}"),
460                                )
461                            })?,
462                        );
463                    }
464                } else {
465                    // Ignore offset and count parameter as these parameters are held separately and not used in the where clause.
466                }
467            }
468            ParsedParameter::Resource(_) => {
469                return Err(OperationOutcomeError::fatal(
470                    IssueType::not_supported(),
471                    format!(
472                        "Parameter '{}' is not supported for history requests.",
473                        parameter.name()
474                    ),
475                ));
476            }
477        }
478    }
479
480    Ok(())
481}
482
483async fn history<'a, 'e, E>(
484    executor: E,
485    tenant: &'a TenantId,
486    project: &'a ProjectId,
487    history_request: &'a HistoryRequest,
488) -> Result<Vec<ResourceHistoryValue>, OperationOutcomeError>
489where
490    E: PgExecutor<'e>,
491{
492    let mut query_builder: QueryBuilder<sqlx::Postgres> =
493        QueryBuilder::new(r"SELECT resource, request_method FROM resources WHERE ");
494
495    let mut clauses = query_builder.separated(" AND ");
496    clauses
497        .push(" tenant = ")
498        .push_bind_unseparated(tenant.as_ref())
499        .push(" project = ")
500        .push_bind_unseparated(project.as_ref());
501
502    let history_parameters = match history_request {
503        HistoryRequest::Instance(history_instance_request) => &history_instance_request.parameters,
504        HistoryRequest::Type(history_type_request) => &history_type_request.parameters,
505        HistoryRequest::System(system_request) => &system_request.parameters,
506    };
507
508    process_history_parameters(history_parameters, &mut clauses)?;
509
510    match history_request {
511        HistoryRequest::Instance(history_instance_request) => {
512            clauses
513                .push(" resource_type = ")
514                .push_bind_unseparated(history_instance_request.resource_type.as_ref())
515                .push(" id = ")
516                .push_bind_unseparated(&history_instance_request.id);
517        }
518        HistoryRequest::Type(history_type_request) => {
519            clauses
520                .push(" resource_type = ")
521                .push_bind_unseparated(history_type_request.resource_type.as_ref());
522        }
523        HistoryRequest::System(_request) => {}
524    }
525
526    let limit = if let Some(ParsedParameter::Result(count_param)) = history_parameters.get("_count")
527    {
528        std::cmp::min(
529            1000,
530            count_param
531                .value
532                .first()
533                .and_then(|v| v.parse::<i64>().ok())
534                .unwrap_or(100),
535        )
536    } else {
537        1000
538    };
539
540    if limit < 0 {
541        return Err(OperationOutcomeError::fatal(
542            IssueType::invalid(),
543            "Invalid _count parameter value. Must be greater than or equal to 0.".to_string(),
544        ));
545    }
546
547    query_builder
548        .push(" ORDER BY sequence DESC LIMIT ")
549        .push_bind(limit);
550
551    if let Some(ParsedParameter::Result(offset_param)) = history_parameters.get("_offset") {
552        let offset = offset_param
553            .value
554            .first()
555            .and_then(|v| v.parse::<i64>().ok())
556            .unwrap_or(0);
557
558        if offset < 0 {
559            return Err(OperationOutcomeError::fatal(
560                IssueType::invalid(),
561                "Invalid _offset parameter value. Must be greater than or equal to 0.".to_string(),
562            ));
563        }
564
565        query_builder.push(" OFFSET ").push_bind(offset);
566    }
567
568    let query = query_builder.build_query_as::<HistoryValue>();
569
570    let result: Vec<HistoryValue> = query.fetch_all(executor).await.map_err(StoreError::from)?;
571
572    Ok(result
573        .into_iter()
574        .map(|r| ResourceHistoryValue {
575            resource: r.resource.0,
576            request_method: r.request_method,
577        })
578        .collect::<Vec<_>>())
579}