cratestack_sqlx/descriptor/
event_enqueue.rs1use cratestack_core::{
7 CratestackError, CratestackEventEnvelope, ModelEventKind, model_event_channel,
8};
9
10use super::OutboxColumn;
11use crate::backplane::notify_payload;
12use crate::error::cratestack_error_from_sqlx;
13use crate::sqlx;
14
15pub async fn enqueue_event_outbox<'e, E, T>(
27 executor: E,
28 model: &str,
29 operation: ModelEventKind,
30 data: &T,
31) -> Result<(), CratestackError>
32where
33 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
34 T: serde::Serialize,
35{
36 enqueue(executor, model, true, operation, data, None).await
39}
40
41pub(crate) async fn enqueue_model_event<'e, E, T>(
44 executor: E,
45 target: cratestack_sql::EventTarget,
46 operation: ModelEventKind,
47 data: &T,
48) -> Result<(), CratestackError>
49where
50 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
51 T: serde::Serialize,
52{
53 enqueue(
54 executor,
55 target.model,
56 target.notify_subscribers,
57 operation,
58 data,
59 None,
60 )
61 .await
62}
63
64pub(crate) async fn enqueue_model_update<'e, E, T>(
69 executor: E,
70 target: cratestack_sql::EventTarget,
71 data: &T,
72 before: Option<&T>,
73 column: Option<&OutboxColumn>,
74) -> Result<(), CratestackError>
75where
76 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
77 T: serde::Serialize,
78{
79 let before = before.filter(|_| column.is_some());
80 let result = enqueue(
81 executor,
82 target.model,
83 target.notify_subscribers,
84 ModelEventKind::Updated,
85 data,
86 before,
87 )
88 .await;
89 if let (Some(column), Err(error)) = (column, &result)
91 && before.is_some()
92 && error.db_sqlstate() == Some("42703")
93 {
94 column.forget();
95 }
96 result
97}
98
99const INSERT: &str = "WITH inserted AS (\
102 INSERT INTO cratestack_event_outbox (event_id, model, operation, occurred_at, payload) \
103 VALUES ($1, $2, $3, $4, $5) RETURNING event_id\
104 ) SELECT pg_notify($6, $7) FROM inserted WHERE $7 <> ''";
105const INSERT_WITH_BEFORE: &str = "WITH inserted AS (\
106 INSERT INTO cratestack_event_outbox \
107 (event_id, model, operation, occurred_at, payload, payload_before) \
108 VALUES ($1, $2, $3, $4, $5, $8) RETURNING event_id\
109 ) SELECT pg_notify($6, $7) FROM inserted WHERE $7 <> ''";
110
111async fn enqueue<'e, E, T>(
112 executor: E,
113 model: &str,
114 notify: bool,
115 operation: ModelEventKind,
116 data: &T,
117 before: Option<&T>,
118) -> Result<(), CratestackError>
119where
120 E: sqlx::Executor<'e, Database = sqlx::Postgres>,
121 T: serde::Serialize,
122{
123 let encode = |value: &T| {
124 serde_json::to_value(value).map_err(|error| {
125 CratestackError::Codec(format!("failed to encode event payload: {error}"))
126 })
127 };
128 let payload = encode(data)?;
129 let envelope = CratestackEventEnvelope {
130 event_id: uuid::Uuid::new_v4(),
131 model: model.to_owned(),
132 operation,
133 occurred_at: chrono::Utc::now(),
134 data: payload,
135 before: before.map(encode).transpose()?,
136 };
137 let envelope_json = serde_json::to_string(&envelope).map_err(|error| {
142 CratestackError::Codec(format!("failed to encode event envelope: {error}"))
143 })?;
144 let notification = if notify {
145 notify_payload::encode(
146 &model_event_channel(model),
147 &envelope_json,
148 Some(envelope.event_id),
149 )
150 .unwrap_or_default()
151 } else {
152 String::new()
153 };
154 let query = sqlx::query(if envelope.before.is_some() {
157 INSERT_WITH_BEFORE
158 } else {
159 INSERT
160 })
161 .bind(envelope.event_id)
162 .bind(model)
163 .bind(operation.as_str())
164 .bind(envelope.occurred_at)
165 .bind(envelope.data)
166 .bind(notify_payload::NOTIFY_CHANNEL)
167 .bind(notification);
168 let query = match envelope.before {
169 Some(before) => query.bind(before),
170 None => query,
171 };
172 query
173 .execute(executor)
174 .await
175 .map_err(cratestack_error_from_sqlx)?;
176
177 Ok(())
178}