Skip to main content

keepsake_sqlx/repository/
timed.rs

1use std::collections::BTreeMap;
2
3use chrono::{DateTime, Utc};
4use keepsake::{RelationDefinition, RelationId, RelationSpec, SubjectRef};
5use uuid::Uuid;
6
7use super::{
8    AppliedKeepsake, KeepsakeRepository, NoopRelationCache, RelationCache, RepositoryResult,
9    TimedExpiryCandidate,
10};
11
12/// Timestamp-scoped repository view.
13///
14/// This wrapper does not read the system clock. Callers choose the timestamp once
15/// at an operation boundary, then use the forwarding methods to keep related
16/// writes and expiry scans on the same deterministic instant.
17#[derive(Debug, Clone, Copy)]
18pub struct TimedKeepsakeRepository<'repo, C = NoopRelationCache> {
19    pub(super) repository: &'repo KeepsakeRepository<C>,
20    pub(super) at: DateTime<Utc>,
21}
22
23impl<'repo, C> TimedKeepsakeRepository<'repo, C>
24where
25    C: RelationCache,
26{
27    /// Returns the repository backing this timestamp-scoped view.
28    #[must_use]
29    pub const fn repository(&self) -> &'repo KeepsakeRepository<C> {
30        self.repository
31    }
32
33    /// Returns the timestamp applied by forwarding methods.
34    #[must_use]
35    pub const fn timestamp(&self) -> DateTime<Utc> {
36        self.at
37    }
38
39    /// Inserts or updates a relation definition using this view's timestamp.
40    pub async fn upsert_relation(
41        &self,
42        relation: &RelationDefinition,
43    ) -> RepositoryResult<RelationDefinition> {
44        self.repository.upsert_relation(relation, self.at).await
45    }
46
47    /// Inserts or updates a typed relation spec using this view's timestamp.
48    pub async fn upsert_relation_spec<Spec>(&self) -> RepositoryResult<RelationDefinition>
49    where
50        Spec: RelationSpec,
51    {
52        self.repository.upsert_relation_spec::<Spec>(self.at).await
53    }
54
55    /// Enables or disables a relation using this view's timestamp.
56    pub async fn set_relation_enabled(
57        &self,
58        relation_id: RelationId,
59        enabled: bool,
60    ) -> RepositoryResult<bool> {
61        self.repository
62            .set_relation_enabled(relation_id, enabled, self.at)
63            .await
64    }
65
66    /// Applies a keepsake relation idempotently using this view's timestamp.
67    pub async fn apply(
68        &self,
69        subject: &SubjectRef,
70        relation_id: RelationId,
71        metadata: &BTreeMap<String, String>,
72    ) -> RepositoryResult<AppliedKeepsake> {
73        self.repository
74            .apply(subject, relation_id, self.at, metadata)
75            .await
76    }
77
78    /// Applies a typed keepsake relation idempotently using this view's timestamp.
79    pub async fn apply_spec<Spec>(
80        &self,
81        subject: &SubjectRef,
82        metadata: &BTreeMap<String, String>,
83    ) -> RepositoryResult<AppliedKeepsake>
84    where
85        Spec: RelationSpec,
86    {
87        self.repository
88            .apply_spec::<Spec>(subject, self.at, metadata)
89            .await
90    }
91
92    /// Applies a keepsake relation with empty metadata using this view's timestamp.
93    pub async fn apply_without_metadata(
94        &self,
95        subject: &SubjectRef,
96        relation_id: RelationId,
97    ) -> RepositoryResult<AppliedKeepsake> {
98        self.repository
99            .apply_without_metadata(subject, relation_id, self.at)
100            .await
101    }
102
103    /// Applies a typed keepsake relation with empty metadata using this view's timestamp.
104    pub async fn apply_spec_without_metadata<Spec>(
105        &self,
106        subject: &SubjectRef,
107    ) -> RepositoryResult<AppliedKeepsake>
108    where
109        Spec: RelationSpec,
110    {
111        self.repository
112            .apply_spec_without_metadata::<Spec>(subject, self.at)
113            .await
114    }
115
116    /// Revokes an active keepsake using this view's timestamp.
117    pub async fn revoke(&self, keepsake_id: Uuid) -> RepositoryResult<bool> {
118        self.repository.revoke(keepsake_id, self.at).await
119    }
120
121    /// Lists due timed expiry candidates using this view's timestamp.
122    pub async fn due_timed_expiry(
123        &self,
124        limit: i64,
125    ) -> RepositoryResult<Vec<TimedExpiryCandidate>> {
126        self.repository.due_timed_expiry(self.at, limit).await
127    }
128
129    /// Expires a stable batch of due timed keepsakes using this view's timestamp.
130    pub async fn expire_due_timed(&self, limit: i64) -> RepositoryResult<u64> {
131        self.repository.expire_due_timed(self.at, limit).await
132    }
133
134    /// Upserts a simple fulfillment counter projection using this view's timestamp.
135    #[cfg(feature = "fulfillment-counters")]
136    pub async fn upsert_counter_projection(
137        &self,
138        keepsake_id: Uuid,
139        key: &str,
140        value: i64,
141    ) -> RepositoryResult<()> {
142        self.repository
143            .upsert_counter_projection(keepsake_id, key, value, self.at)
144            .await
145    }
146}