1use async_trait::async_trait;
21use chrono::{DateTime, Utc};
22use sqlx::PgConnection;
23use sqlx::postgres::PgRow;
24use turnframe_core::case::CaseKey;
25use turnframe_core::event::EventRedaction;
26use turnframe_core::ids::{AccountId, CaseRevision, CommandId, EventId, RedactionAuthority};
27use turnframe_store::error::{StoreError, invalid_record};
28use turnframe_store::events::{
29 EventBatch, EventCursor, EventJournalReader, EventJournalWriter, EventPage, StoredEvent,
30};
31use uuid::Uuid;
32
33use crate::codec::{
34 column, count_from_sql, cursor_to_sql, limit_to_sql, revision_from_sql, revision_to_sql,
35 sequence_from_sql,
36};
37use crate::error::store_error;
38use crate::store::{PgStores, commit};
39
40const EVENT_COLUMNS: &str = "sequence, event_id, workflow_key, case_id, case_revision, \
42 command_id, event_type, payload, occurred_at, redacted_at, redaction_authority";
43
44pub(crate) async fn append(
46 conn: &mut PgConnection,
47 batch: EventBatch,
48) -> Result<Vec<EventId>, StoreError> {
49 if batch.is_empty() {
50 return Err(invalid_record());
51 }
52 let ids: Vec<Uuid> = batch.events.iter().map(|e| *e.event_id.as_uuid()).collect();
53 let types: Vec<String> = batch.events.iter().map(|e| e.event_type.clone()).collect();
54 let payloads: Vec<serde_json::Value> = batch.events.iter().map(|e| e.payload.clone()).collect();
55 let occurred: Vec<DateTime<Utc>> = batch.events.iter().map(|e| e.occurred_at).collect();
56 sqlx::query(
59 "INSERT INTO tf_domain_event (
60 account_id, event_id, workflow_key, case_id, case_revision, command_id,
61 event_type, payload, occurred_at
62 )
63 SELECT $1, e.event_id, $2, $3, $4, $5, e.event_type, e.payload, e.occurred_at
64 FROM unnest($6::uuid[], $7::text[], $8::jsonb[], $9::timestamptz[])
65 WITH ORDINALITY AS e(event_id, event_type, payload, occurred_at, position)
66 ORDER BY e.position",
67 )
68 .bind(batch.account_id.as_str())
69 .bind(batch.case_key.workflow.as_str())
70 .bind(batch.case_key.case_id.as_str())
71 .bind(revision_to_sql(batch.revision)?)
72 .bind(batch.command_id.as_uuid())
73 .bind(ids)
74 .bind(types)
75 .bind(payloads)
76 .bind(occurred)
77 .execute(conn)
78 .await
79 .map_err(|error| store_error(&error))?;
80 Ok(batch.event_ids())
81}
82
83pub(crate) async fn list_since(
85 conn: &mut PgConnection,
86 account: &AccountId,
87 case_key: &CaseKey,
88 since: CaseRevision,
89 limit: usize,
90) -> Result<Vec<StoredEvent>, StoreError> {
91 let statement = format!(
92 "SELECT {EVENT_COLUMNS} FROM tf_domain_event
93 WHERE account_id = $1 AND workflow_key = $2 AND case_id = $3 AND case_revision > $4
94 ORDER BY sequence
95 LIMIT $5"
96 );
97 let rows = sqlx::query(&statement)
98 .bind(account.as_str())
99 .bind(case_key.workflow.as_str())
100 .bind(case_key.case_id.as_str())
101 .bind(revision_to_sql(since)?)
102 .bind(limit_to_sql(limit))
103 .fetch_all(conn)
104 .await
105 .map_err(|error| store_error(&error))?;
106 rows.iter().map(|row| decode_event(row, account)).collect()
107}
108
109pub(crate) async fn read_from(
118 conn: &mut PgConnection,
119 account: &AccountId,
120 after: EventCursor,
121 limit: usize,
122) -> Result<EventPage, StoreError> {
123 let statement = format!(
124 "SELECT {EVENT_COLUMNS} FROM tf_domain_event
125 WHERE account_id = $1 AND sequence > $2
126 ORDER BY sequence
127 LIMIT $3"
128 );
129 let rows = sqlx::query(&statement)
130 .bind(account.as_str())
131 .bind(cursor_to_sql(after))
132 .bind(limit_to_sql(limit))
133 .fetch_all(conn)
134 .await
135 .map_err(|error| store_error(&error))?;
136 let events = rows
137 .iter()
138 .map(|row| decode_event(row, account))
139 .collect::<Result<Vec<StoredEvent>, StoreError>>()?;
140 Ok(EventPage::new(events, after))
141}
142
143pub(crate) async fn get_by_ids(
149 conn: &mut PgConnection,
150 account: &AccountId,
151 ids: &[EventId],
152) -> Result<Vec<StoredEvent>, StoreError> {
153 if ids.is_empty() {
154 return Ok(Vec::new());
155 }
156 let wanted: Vec<Uuid> = ids.iter().map(|id| *id.as_uuid()).collect();
157 let statement = format!(
158 "SELECT {EVENT_COLUMNS} FROM tf_domain_event
159 WHERE account_id = $1 AND event_id = ANY($2)"
160 );
161 let rows = sqlx::query(&statement)
162 .bind(account.as_str())
163 .bind(wanted)
164 .fetch_all(conn)
165 .await
166 .map_err(|error| store_error(&error))?;
167 let found = rows
168 .iter()
169 .map(|row| decode_event(row, account))
170 .collect::<Result<Vec<StoredEvent>, StoreError>>()?;
171 Ok(ids
172 .iter()
173 .filter_map(|id| found.iter().find(|event| event.event_id == *id).cloned())
174 .collect())
175}
176
177pub(crate) async fn redact_payload(
190 conn: &mut PgConnection,
191 account: &AccountId,
192 event_id: &EventId,
193 authority: &RedactionAuthority,
194) -> Result<EventRedaction, StoreError> {
195 let row = sqlx::query(
196 "UPDATE tf_domain_event
197 SET payload = 'null'::jsonb,
198 redacted_at = COALESCE(redacted_at, now()),
199 redaction_authority = COALESCE(redaction_authority, $3)
200 WHERE account_id = $1 AND event_id = $2
201 RETURNING redacted_at, redaction_authority",
202 )
203 .bind(account.as_str())
204 .bind(event_id.as_uuid())
205 .bind(authority.as_str())
206 .fetch_optional(conn)
207 .await
208 .map_err(|error| store_error(&error))?
209 .ok_or(StoreError::NotFound)?;
210 decode_redaction(&row)?.ok_or(StoreError::Corrupt)
211}
212
213pub(crate) async fn count(
215 conn: &mut PgConnection,
216 account: &AccountId,
217 case_key: &CaseKey,
218) -> Result<u64, StoreError> {
219 let row = sqlx::query(
220 "SELECT count(*) AS total FROM tf_domain_event
221 WHERE account_id = $1 AND workflow_key = $2 AND case_id = $3",
222 )
223 .bind(account.as_str())
224 .bind(case_key.workflow.as_str())
225 .bind(case_key.case_id.as_str())
226 .fetch_one(conn)
227 .await
228 .map_err(|error| store_error(&error))?;
229 count_from_sql(column(&row, "total")?)
230}
231
232fn decode_redaction(row: &PgRow) -> Result<Option<EventRedaction>, StoreError> {
237 let redacted_at: Option<DateTime<Utc>> = column(row, "redacted_at")?;
238 let authority: Option<String> = column(row, "redaction_authority")?;
239 match (redacted_at, authority) {
240 (None, None) => Ok(None),
241 (Some(redacted_at), Some(authority)) => Ok(Some(EventRedaction {
242 redacted_at,
243 authority: RedactionAuthority::from(authority),
244 })),
245 _ => Err(StoreError::Corrupt),
246 }
247}
248
249fn decode_event(row: &PgRow, account: &AccountId) -> Result<StoredEvent, StoreError> {
251 let sequence: i64 = column(row, "sequence")?;
252 let event_id: Uuid = column(row, "event_id")?;
253 let workflow_key: String = column(row, "workflow_key")?;
254 let case_id: String = column(row, "case_id")?;
255 let revision: i64 = column(row, "case_revision")?;
256 let command_id: Uuid = column(row, "command_id")?;
257 Ok(StoredEvent {
258 sequence: sequence_from_sql(sequence)?,
259 event_id: EventId::from(event_id),
260 account_id: account.clone(),
261 case_key: CaseKey::new(workflow_key, case_id),
262 case_revision: revision_from_sql(revision)?,
263 command_id: CommandId::from(command_id),
264 event_type: column(row, "event_type")?,
265 payload: column(row, "payload")?,
266 occurred_at: column(row, "occurred_at")?,
267 redaction: decode_redaction(row)?,
268 })
269}
270
271#[async_trait]
272impl EventJournalReader for PgStores {
273 async fn list_since(
274 &self,
275 account: &AccountId,
276 case_key: &CaseKey,
277 since: CaseRevision,
278 limit: usize,
279 ) -> Result<Vec<StoredEvent>, StoreError> {
280 let mut conn = self.connection().await?;
281 list_since(&mut conn, account, case_key, since, limit).await
282 }
283
284 async fn read_from(
285 &self,
286 account: &AccountId,
287 after: EventCursor,
288 limit: usize,
289 ) -> Result<EventPage, StoreError> {
290 let mut conn = self.connection().await?;
291 read_from(&mut conn, account, after, limit).await
292 }
293
294 async fn get_by_ids(
295 &self,
296 account: &AccountId,
297 ids: &[EventId],
298 ) -> Result<Vec<StoredEvent>, StoreError> {
299 let mut conn = self.connection().await?;
300 get_by_ids(&mut conn, account, ids).await
301 }
302
303 async fn count(&self, account: &AccountId, case_key: &CaseKey) -> Result<u64, StoreError> {
304 let mut conn = self.connection().await?;
305 count(&mut conn, account, case_key).await
306 }
307}
308
309#[async_trait]
310impl EventJournalWriter for PgStores {
311 async fn append(&self, batch: EventBatch) -> Result<Vec<EventId>, StoreError> {
312 let mut transaction = self.transaction().await?;
313 let ids = append(&mut transaction, batch).await?;
314 commit(transaction).await?;
315 Ok(ids)
316 }
317
318 async fn redact_payload(
319 &self,
320 account: &AccountId,
321 event_id: &EventId,
322 authority: &RedactionAuthority,
323 ) -> Result<EventRedaction, StoreError> {
324 let mut transaction = self.transaction().await?;
325 let record = redact_payload(&mut transaction, account, event_id, authority).await?;
326 commit(transaction).await?;
327 Ok(record)
328 }
329}