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/// ¶ms,
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}