cratestack_sqlx/descriptor/
event_outbox.rs1use cratestack_core::{CratestackError, CratestackEventEnvelope, ModelEventKind};
8
9use crate::error::cratestack_error_from_sqlx;
10use crate::sqlx;
11
12#[derive(Debug, Clone)]
13pub(crate) struct EventOutboxRow {
14 pub(crate) event_id: uuid::Uuid,
15 pub(crate) model: String,
16 pub(crate) operation: String,
17 pub(crate) occurred_at: chrono::DateTime<chrono::Utc>,
18 pub(crate) payload: serde_json::Value,
19 pub(crate) attempts: i64,
20 pub(crate) last_error: Option<String>,
21}
22
23impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for EventOutboxRow {
27 fn from_row(row: &'r sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
28 use sqlx::Row;
29 Ok(Self {
30 event_id: row.try_get("event_id")?,
31 model: row.try_get("model")?,
32 operation: row.try_get("operation")?,
33 occurred_at: row.try_get("occurred_at")?,
34 payload: row.try_get("payload")?,
35 attempts: row.try_get("attempts")?,
36 last_error: row.try_get("last_error")?,
37 })
38 }
39}
40
41impl EventOutboxRow {
42 pub(crate) fn try_into_envelope(self) -> Result<CratestackEventEnvelope, CratestackError> {
43 let _ = self.attempts;
44 let _ = &self.last_error;
45 Ok(CratestackEventEnvelope {
46 event_id: self.event_id,
47 model: self.model,
48 operation: ModelEventKind::parse(&self.operation)?,
49 occurred_at: self.occurred_at,
50 data: self.payload,
51 })
52 }
53}
54
55pub async fn ensure_event_outbox_table<'e, E>(executor: E) -> Result<(), CratestackError>
62where
63 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
64{
65 sqlx::query(
66 "CREATE TABLE IF NOT EXISTS cratestack_event_outbox (\
67 event_id UUID PRIMARY KEY, \
68 model TEXT NOT NULL, \
69 operation TEXT NOT NULL, \
70 occurred_at TIMESTAMPTZ NOT NULL, \
71 payload JSONB NOT NULL, \
72 delivered_at TIMESTAMPTZ, \
73 attempts BIGINT NOT NULL DEFAULT 0, \
74 last_error TEXT\
75 )",
76 )
77 .execute(executor)
78 .await
79 .map_err(cratestack_error_from_sqlx)?;
80
81 Ok(())
82}
83
84pub async fn enqueue_event_outbox<'e, E, T>(
96 executor: E,
97 model: &str,
98 operation: ModelEventKind,
99 data: &T,
100) -> Result<(), CratestackError>
101where
102 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
103 T: serde::Serialize,
104{
105 let payload = serde_json::to_value(data).map_err(|error| {
106 CratestackError::Codec(format!("failed to encode event payload: {error}"))
107 })?;
108 sqlx::query(
109 "INSERT INTO cratestack_event_outbox (event_id, model, operation, occurred_at, payload) \
110 VALUES ($1, $2, $3, $4, $5)",
111 )
112 .bind(uuid::Uuid::new_v4())
113 .bind(model)
114 .bind(operation.as_str())
115 .bind(chrono::Utc::now())
116 .bind(payload)
117 .execute(executor)
118 .await
119 .map_err(cratestack_error_from_sqlx)?;
120
121 Ok(())
122}