Skip to main content

helios_persistence/core/
history.rs

1//! History provider traits.
2//!
3//! This module defines a progressive trait hierarchy for history operations:
4//! - [`InstanceHistoryProvider`] - History for a single resource instance
5//! - [`TypeHistoryProvider`] - History for all resources of a type
6//! - [`SystemHistoryProvider`] - History across all resource types
7//!
8//! Backends implement the levels they support, with each level extending the previous.
9
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14use crate::error::StorageResult;
15use crate::tenant::TenantContext;
16use crate::types::{Page, Pagination, StoredResource};
17
18use super::versioned::VersionedStorage;
19
20/// Parameters for history queries.
21#[derive(Debug, Clone, Default)]
22pub struct HistoryParams {
23    /// Only include versions created/updated since this time.
24    pub since: Option<DateTime<Utc>>,
25
26    /// Only include versions created/updated before this time.
27    pub before: Option<DateTime<Utc>>,
28
29    /// Pagination settings.
30    pub pagination: Pagination,
31
32    /// If true, include deleted versions.
33    pub include_deleted: bool,
34}
35
36impl HistoryParams {
37    /// Creates new history parameters with defaults.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Sets the since filter.
43    pub fn since(mut self, since: DateTime<Utc>) -> Self {
44        self.since = Some(since);
45        self
46    }
47
48    /// Sets the before filter.
49    pub fn before(mut self, before: DateTime<Utc>) -> Self {
50        self.before = Some(before);
51        self
52    }
53
54    /// Sets the count limit.
55    pub fn count(mut self, count: u32) -> Self {
56        self.pagination = self.pagination.with_count(count);
57        self
58    }
59
60    /// Sets whether to include deleted versions.
61    pub fn include_deleted(mut self, include: bool) -> Self {
62        self.include_deleted = include;
63        self
64    }
65}
66
67/// A single entry in a history bundle.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct HistoryEntry {
70    /// The resource at this version.
71    pub resource: StoredResource,
72
73    /// The HTTP method that created this version.
74    pub method: HistoryMethod,
75
76    /// When this version was created.
77    pub timestamp: DateTime<Utc>,
78}
79
80/// HTTP method that created a history entry.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "UPPERCASE")]
83pub enum HistoryMethod {
84    /// Resource was created (POST).
85    Post,
86    /// Resource was updated (PUT).
87    Put,
88    /// Resource was patched (PATCH).
89    Patch,
90    /// Resource was deleted (DELETE).
91    Delete,
92}
93
94impl std::fmt::Display for HistoryMethod {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            HistoryMethod::Post => write!(f, "POST"),
98            HistoryMethod::Put => write!(f, "PUT"),
99            HistoryMethod::Patch => write!(f, "PATCH"),
100            HistoryMethod::Delete => write!(f, "DELETE"),
101        }
102    }
103}
104
105/// A page of history entries.
106pub type HistoryPage = Page<HistoryEntry>;
107
108/// Provider for instance-level history.
109///
110/// This trait provides the history for a single resource instance,
111/// corresponding to the FHIR history interaction:
112/// `GET [base]/[type]/[id]/_history`
113///
114/// # Example
115///
116/// ```ignore
117/// use helios_persistence::core::InstanceHistoryProvider;
118///
119/// async fn get_patient_history<S: InstanceHistoryProvider>(
120///     storage: &S,
121///     tenant: &TenantContext,
122/// ) -> Result<(), StorageError> {
123///     let params = HistoryParams::new()
124///         .since(Utc::now() - Duration::days(30))
125///         .count(10);
126///
127///     let history = storage.history_instance(
128///         tenant,
129///         "Patient",
130///         "123",
131///         &params,
132///     ).await?;
133///
134///     for entry in history.items {
135///         println!("Version {}: {} at {}",
136///             entry.resource.version_id(),
137///             entry.method,
138///             entry.timestamp
139///         );
140///     }
141///
142///     Ok(())
143/// }
144/// ```
145#[async_trait]
146pub trait InstanceHistoryProvider: VersionedStorage {
147    /// Gets the history for a specific resource instance.
148    ///
149    /// # Arguments
150    ///
151    /// * `tenant` - The tenant context for this operation
152    /// * `resource_type` - The FHIR resource type
153    /// * `id` - The resource's logical ID
154    /// * `params` - History query parameters
155    ///
156    /// # Returns
157    ///
158    /// A page of history entries in reverse chronological order (newest first).
159    async fn history_instance(
160        &self,
161        tenant: &TenantContext,
162        resource_type: &str,
163        id: &str,
164        params: &HistoryParams,
165    ) -> StorageResult<HistoryPage>;
166
167    /// Gets the total number of versions for a resource.
168    async fn history_instance_count(
169        &self,
170        tenant: &TenantContext,
171        resource_type: &str,
172        id: &str,
173    ) -> StorageResult<u64>;
174
175    /// Deletes all history for a specific resource instance.
176    ///
177    /// This is a FHIR v6.0.0 Trial Use feature:
178    /// `DELETE [base]/[type]/[id]/_history`
179    ///
180    /// After this operation, the resource's history is cleared but the current
181    /// version may optionally be preserved (implementation-defined).
182    ///
183    /// # Arguments
184    ///
185    /// * `tenant` - The tenant context for this operation
186    /// * `resource_type` - The FHIR resource type
187    /// * `id` - The resource's logical ID
188    ///
189    /// # Returns
190    ///
191    /// The number of history entries deleted.
192    ///
193    /// # Errors
194    ///
195    /// * `StorageError::Resource(NotFound)` - If the resource doesn't exist
196    /// * `StorageError::Tenant` - If the tenant doesn't have delete permission
197    /// * `StorageError::Backend(NotSupported)` - If delete history is not supported
198    async fn delete_instance_history(
199        &self,
200        tenant: &TenantContext,
201        resource_type: &str,
202        id: &str,
203    ) -> StorageResult<u64> {
204        // Default implementation returns UnsupportedCapability
205        let _ = (tenant, resource_type, id);
206        Err(crate::error::StorageError::Backend(
207            crate::error::BackendError::UnsupportedCapability {
208                backend_name: "unknown".to_string(),
209                capability: "delete_instance_history".to_string(),
210            },
211        ))
212    }
213
214    /// Deletes a specific version from a resource's history.
215    ///
216    /// This is a FHIR v6.0.0 Trial Use feature:
217    /// `DELETE [base]/[type]/[id]/_history/[vid]`
218    ///
219    /// Deleting the current version may have special semantics depending on
220    /// the implementation (e.g., promoting the previous version or failing).
221    ///
222    /// # Arguments
223    ///
224    /// * `tenant` - The tenant context for this operation
225    /// * `resource_type` - The FHIR resource type
226    /// * `id` - The resource's logical ID
227    /// * `version_id` - The specific version to delete
228    ///
229    /// # Errors
230    ///
231    /// * `StorageError::Resource(VersionNotFound)` - If the version doesn't exist
232    /// * `StorageError::Tenant` - If the tenant doesn't have delete permission
233    /// * `StorageError::Backend(NotSupported)` - If delete version is not supported
234    async fn delete_version(
235        &self,
236        tenant: &TenantContext,
237        resource_type: &str,
238        id: &str,
239        version_id: &str,
240    ) -> StorageResult<()> {
241        // Default implementation returns NotSupported
242        let _ = (tenant, resource_type, id, version_id);
243        Err(crate::error::StorageError::Backend(
244            crate::error::BackendError::UnsupportedCapability {
245                backend_name: "unknown".to_string(),
246                capability: "delete_version".to_string(),
247            },
248        ))
249    }
250}
251
252/// Provider for type-level history.
253///
254/// This trait provides the history for all resources of a given type,
255/// corresponding to the FHIR history interaction:
256/// `GET [base]/[type]/_history`
257///
258/// This extends [`InstanceHistoryProvider`] as backends that support type-level
259/// history also support instance-level history.
260#[async_trait]
261pub trait TypeHistoryProvider: InstanceHistoryProvider {
262    /// Gets the history for all resources of a type.
263    ///
264    /// # Arguments
265    ///
266    /// * `tenant` - The tenant context for this operation
267    /// * `resource_type` - The FHIR resource type
268    /// * `params` - History query parameters
269    ///
270    /// # Returns
271    ///
272    /// A page of history entries in reverse chronological order.
273    async fn history_type(
274        &self,
275        tenant: &TenantContext,
276        resource_type: &str,
277        params: &HistoryParams,
278    ) -> StorageResult<HistoryPage>;
279
280    /// Gets the total number of history entries for a resource type.
281    async fn history_type_count(
282        &self,
283        tenant: &TenantContext,
284        resource_type: &str,
285    ) -> StorageResult<u64>;
286}
287
288/// Provider for system-level history.
289///
290/// This trait provides the history across all resource types,
291/// corresponding to the FHIR history interaction:
292/// `GET [base]/_history`
293///
294/// This extends [`TypeHistoryProvider`] as backends that support system-level
295/// history also support type-level and instance-level history.
296#[async_trait]
297pub trait SystemHistoryProvider: TypeHistoryProvider {
298    /// Gets the history for all resources in the system.
299    ///
300    /// # Arguments
301    ///
302    /// * `tenant` - The tenant context for this operation
303    /// * `params` - History query parameters
304    ///
305    /// # Returns
306    ///
307    /// A page of history entries in reverse chronological order.
308    async fn history_system(
309        &self,
310        tenant: &TenantContext,
311        params: &HistoryParams,
312    ) -> StorageResult<HistoryPage>;
313
314    /// Gets the total number of history entries in the system.
315    async fn history_system_count(&self, tenant: &TenantContext) -> StorageResult<u64>;
316}
317
318/// Extension trait for history providers that support differential queries.
319///
320/// Differential queries return only resources that have changed since a given point,
321/// which is more efficient for synchronization use cases.
322#[async_trait]
323pub trait DifferentialHistoryProvider: TypeHistoryProvider {
324    /// Gets resources modified since a given timestamp.
325    ///
326    /// This is more efficient than full history for sync scenarios as it returns
327    /// only the current version of each modified resource, not all versions.
328    ///
329    /// # Arguments
330    ///
331    /// * `tenant` - The tenant context for this operation
332    /// * `resource_type` - The FHIR resource type (or None for all types)
333    /// * `since` - Only include resources modified after this time
334    /// * `pagination` - Pagination settings
335    ///
336    /// # Returns
337    ///
338    /// A page of current resource versions that were modified since the given time.
339    async fn modified_since(
340        &self,
341        tenant: &TenantContext,
342        resource_type: Option<&str>,
343        since: DateTime<Utc>,
344        pagination: &Pagination,
345    ) -> StorageResult<Page<StoredResource>>;
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use helios_fhir::FhirVersion;
352
353    #[test]
354    fn test_history_params_builder() {
355        let now = Utc::now();
356        let params = HistoryParams::new()
357            .since(now)
358            .count(50)
359            .include_deleted(true);
360
361        assert!(params.since.is_some());
362        assert_eq!(params.pagination.count, 50);
363        assert!(params.include_deleted);
364    }
365
366    #[test]
367    fn test_history_method_display() {
368        assert_eq!(HistoryMethod::Post.to_string(), "POST");
369        assert_eq!(HistoryMethod::Put.to_string(), "PUT");
370        assert_eq!(HistoryMethod::Patch.to_string(), "PATCH");
371        assert_eq!(HistoryMethod::Delete.to_string(), "DELETE");
372    }
373
374    #[test]
375    fn test_history_entry_creation() {
376        let resource = StoredResource::new(
377            "Patient",
378            "123",
379            crate::tenant::TenantId::new("t1"),
380            serde_json::json!({}),
381            FhirVersion::default(),
382        );
383
384        let entry = HistoryEntry {
385            resource,
386            method: HistoryMethod::Post,
387            timestamp: Utc::now(),
388        };
389
390        assert_eq!(entry.method, HistoryMethod::Post);
391    }
392}