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