a2a_protocol_server/push/
postgres_config_store.rs1use std::future::Future;
11use std::pin::Pin;
12
13use a2a_protocol_types::error::{A2aError, A2aResult};
14use a2a_protocol_types::push::TaskPushNotificationConfig;
15use sqlx::postgres::PgPool;
16
17use super::config_store::PushConfigStore;
18
19#[derive(Debug, Clone)]
34pub struct PostgresPushConfigStore {
35 pool: PgPool,
36}
37
38#[allow(clippy::needless_pass_by_value)]
40fn to_a2a_error(e: sqlx::Error) -> A2aError {
41 A2aError::internal(format!("postgres error: {e}"))
42}
43
44impl PostgresPushConfigStore {
45 pub async fn new(url: &str) -> Result<Self, sqlx::Error> {
51 let pool = sqlx::postgres::PgPoolOptions::new()
52 .max_connections(10)
53 .connect(url)
54 .await?;
55 Self::from_pool(pool).await
56 }
57
58 pub async fn from_pool(pool: PgPool) -> Result<Self, sqlx::Error> {
64 sqlx::query(
65 "CREATE TABLE IF NOT EXISTS push_configs (
66 task_id TEXT NOT NULL,
67 id TEXT NOT NULL,
68 data JSONB NOT NULL,
69 PRIMARY KEY (task_id, id)
70 )",
71 )
72 .execute(&pool)
73 .await?;
74
75 Ok(Self { pool })
76 }
77}
78
79#[allow(clippy::manual_async_fn)]
80impl PushConfigStore for PostgresPushConfigStore {
81 fn set<'a>(
82 &'a self,
83 mut config: TaskPushNotificationConfig,
84 ) -> Pin<Box<dyn Future<Output = A2aResult<TaskPushNotificationConfig>> + Send + 'a>> {
85 Box::pin(async move {
86 let Some(task_id) = config.task_id.clone() else {
90 return Err(A2aError::invalid_params(
91 "taskId is required to store a push notification config",
92 ));
93 };
94 let id = config
95 .id
96 .clone()
97 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
98 config.id = Some(id.clone());
99
100 let data = serde_json::to_value(&config)
101 .map_err(|e| A2aError::internal(format!("serialize: {e}")))?;
102
103 sqlx::query(
104 "INSERT INTO push_configs (task_id, id, data)
105 VALUES ($1, $2, $3)
106 ON CONFLICT(task_id, id) DO UPDATE SET data = EXCLUDED.data",
107 )
108 .bind(&task_id)
109 .bind(&id)
110 .bind(&data)
111 .execute(&self.pool)
112 .await
113 .map_err(to_a2a_error)?;
114
115 Ok(config)
116 })
117 }
118
119 fn get<'a>(
120 &'a self,
121 task_id: &'a str,
122 id: &'a str,
123 ) -> Pin<Box<dyn Future<Output = A2aResult<Option<TaskPushNotificationConfig>>> + Send + 'a>>
124 {
125 Box::pin(async move {
126 let row: Option<(serde_json::Value,)> =
127 sqlx::query_as("SELECT data FROM push_configs WHERE task_id = $1 AND id = $2")
128 .bind(task_id)
129 .bind(id)
130 .fetch_optional(&self.pool)
131 .await
132 .map_err(to_a2a_error)?;
133
134 match row {
135 Some((data,)) => {
136 let config: TaskPushNotificationConfig = serde_json::from_value(data)
137 .map_err(|e| A2aError::internal(format!("deserialize: {e}")))?;
138 Ok(Some(config))
139 }
140 None => Ok(None),
141 }
142 })
143 }
144
145 fn list<'a>(
146 &'a self,
147 task_id: &'a str,
148 ) -> Pin<Box<dyn Future<Output = A2aResult<Vec<TaskPushNotificationConfig>>> + Send + 'a>> {
149 Box::pin(async move {
150 let rows: Vec<(serde_json::Value,)> =
151 sqlx::query_as("SELECT data FROM push_configs WHERE task_id = $1")
152 .bind(task_id)
153 .fetch_all(&self.pool)
154 .await
155 .map_err(to_a2a_error)?;
156
157 rows.into_iter()
158 .map(|(data,)| {
159 serde_json::from_value(data)
160 .map_err(|e| A2aError::internal(format!("deserialize: {e}")))
161 })
162 .collect()
163 })
164 }
165
166 fn delete<'a>(
167 &'a self,
168 task_id: &'a str,
169 id: &'a str,
170 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
171 Box::pin(async move {
172 sqlx::query("DELETE FROM push_configs WHERE task_id = $1 AND id = $2")
173 .bind(task_id)
174 .bind(id)
175 .execute(&self.pool)
176 .await
177 .map_err(to_a2a_error)?;
178 Ok(())
179 })
180 }
181
182 fn count(&self) -> Pin<Box<dyn Future<Output = A2aResult<Option<usize>>> + Send + '_>> {
183 Box::pin(async move {
184 let (total,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM push_configs")
185 .fetch_one(&self.pool)
186 .await
187 .map_err(to_a2a_error)?;
188 Ok(Some(usize::try_from(total).unwrap_or(usize::MAX)))
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn to_a2a_error_formats_message() {
199 let pg_err = sqlx::Error::RowNotFound;
200 let a2a_err = to_a2a_error(pg_err);
201 let msg = format!("{a2a_err}");
202 assert!(
203 msg.contains("postgres error"),
204 "error message should contain 'postgres error': {msg}"
205 );
206 }
207}