keepsake_sqlx/repository/
mutation.rs1use keepsake::{
2 ApplyKeepsake, Keepsake, KeepsakeId, RelationDefinition, RelationId, RevokeBySubject,
3 RevokeKeepsake, SubjectRef,
4};
5use sqlx::{Postgres, Transaction};
6use uuid::Uuid;
7
8use super::audit::record_audit_event_tx;
9use super::support::{apply_event, revoke_by_subject_event, revoke_event};
10use super::{
11 AppliedKeepsake, AppliedKeepsakeRow, AppliedKeepsakeWriteRow, KeepsakeRepository,
12 RelationCache, RelationRow, RepositoryError, RepositoryResult,
13};
14
15impl<C> KeepsakeRepository<C>
16where
17 C: RelationCache,
18{
19 pub async fn apply(&self, command: &ApplyKeepsake) -> RepositoryResult<AppliedKeepsake> {
25 command.subject.validate()?;
26 command.context.validate()?;
27
28 let mut tx = self.pool.begin().await?;
29 let relation = relation_for_share_tx(&mut tx, command.relation_id).await?;
30 let metadata = serde_json::to_value(&command.metadata)?;
31
32 let applied = sqlx::query_as::<_, AppliedKeepsakeWriteRow>(
33 r"
34 insert into keepsakes
35 (id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at, expires_at, metadata, created_at, updated_at)
36 select
37 $1,
38 $2,
39 $3,
40 r.id,
41 'applied',
42 r.expiry_policy,
43 $4,
44 case
45 when r.expiry_policy->>'type' = 'at'
46 then (r.expiry_policy->>'timestamp')::timestamptz
47 else null
48 end,
49 $5,
50 $4,
51 $4
52 from keepsake_relation_definitions r
53 where r.id = $6
54 on conflict (subject_kind, subject_id, relation_id) where state = 'applied'
55 do update set updated_at = keepsakes.updated_at
56 returning id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
57 expires_at, fulfilled_at, revoked_at, metadata, (xmax <> 0) as duplicate_prevented
58 ",
59 )
60 .bind(command.id)
61 .bind(command.subject.kind())
62 .bind(command.subject.id())
63 .bind(command.at)
64 .bind(metadata)
65 .bind(command.relation_id)
66 .fetch_one(&mut *tx)
67 .await?;
68
69 if !relation.enabled && !applied.duplicate_prevented {
70 return Err(RepositoryError::RelationDisabled {
71 relation_id: command.relation_id,
72 });
73 }
74
75 let (keepsake, duplicate_prevented) = applied.try_into_parts()?;
76 let event = apply_event(command, &keepsake, duplicate_prevented);
77 record_audit_event_tx(&mut tx, &event).await?;
78 tx.commit().await?;
79 Ok(AppliedKeepsake {
80 keepsake,
81 duplicate_prevented,
82 })
83 }
84
85 pub async fn revoke(&self, command: &RevokeKeepsake) -> RepositoryResult<bool> {
87 command.context.validate()?;
88
89 let mut tx = self.pool.begin().await?;
90 let revoked = revoke_tx(&mut tx, command.keepsake_id, command.at).await?;
91 if let Some(keepsake) = &revoked {
92 let event = revoke_event(command, keepsake);
93 record_audit_event_tx(&mut tx, &event).await?;
94 }
95 tx.commit().await?;
96 Ok(revoked.is_some())
97 }
98
99 pub async fn revoke_by_subject(
104 &self,
105 command: &RevokeBySubject,
106 ) -> RepositoryResult<Option<KeepsakeId>> {
107 command.subject.validate()?;
108 command.context.validate()?;
109
110 let mut tx = self.pool.begin().await?;
111 let revoked =
112 revoke_by_subject_tx(&mut tx, &command.subject, command.relation_id, command.at)
113 .await?;
114 let revoked_id = revoked.as_ref().map(Keepsake::id);
115 if let Some(keepsake) = &revoked {
116 let event = revoke_by_subject_event(command, keepsake);
117 record_audit_event_tx(&mut tx, &event).await?;
118 }
119 tx.commit().await?;
120 Ok(revoked_id)
121 }
122}
123
124async fn revoke_by_subject_tx(
125 tx: &mut Transaction<'_, Postgres>,
126 subject: &SubjectRef,
127 relation_id: RelationId,
128 at: chrono::DateTime<chrono::Utc>,
129) -> RepositoryResult<Option<Keepsake>> {
130 let row = sqlx::query_as::<_, AppliedKeepsakeRow>(
131 r"
132 update keepsakes
133 set state = 'revoked', revoked_at = $4, updated_at = $4
134 where subject_kind = $1 and subject_id = $2 and relation_id = $3 and state = 'applied'
135 returning id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
136 expires_at, fulfilled_at, revoked_at, metadata
137 ",
138 )
139 .bind(subject.kind())
140 .bind(subject.id())
141 .bind(relation_id)
142 .bind(at)
143 .fetch_optional(&mut **tx)
144 .await?;
145
146 row.map(AppliedKeepsakeRow::try_into_keepsake).transpose()
147}
148
149async fn revoke_tx(
150 tx: &mut Transaction<'_, Postgres>,
151 keepsake_id: Uuid,
152 at: chrono::DateTime<chrono::Utc>,
153) -> RepositoryResult<Option<Keepsake>> {
154 let row = sqlx::query_as::<_, AppliedKeepsakeRow>(
155 r"
156 update keepsakes
157 set state = 'revoked', revoked_at = $2, updated_at = $2
158 where id = $1 and state = 'applied'
159 returning id, subject_kind, subject_id, relation_id, state, expiry_policy, applied_at,
160 expires_at, fulfilled_at, revoked_at, metadata
161 ",
162 )
163 .bind(keepsake_id)
164 .bind(at)
165 .fetch_optional(&mut **tx)
166 .await?;
167
168 row.map(AppliedKeepsakeRow::try_into_keepsake).transpose()
169}
170
171async fn relation_for_share_tx(
172 tx: &mut Transaction<'_, Postgres>,
173 relation_id: RelationId,
174) -> RepositoryResult<RelationDefinition> {
175 let row = sqlx::query_as::<_, RelationRow>(
176 r"
177 select id, kind, key, enabled, expiry_policy
178 from keepsake_relation_definitions
179 where id = $1
180 for share
181 ",
182 )
183 .bind(relation_id)
184 .fetch_one(&mut **tx)
185 .await?;
186 row.try_into_relation()
187}