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