secrets_storage_postgres/
lib.rs1use async_trait::async_trait;
2use secrets_core::storage::{StorageBackend, StorageEntry, StorageError, StorageResult};
3use sqlx::PgPool;
4
5pub struct PgStorage {
6 pool: PgPool,
7}
8
9impl PgStorage {
10 pub async fn connect(database_url: &str) -> Result<Self, sqlx::Error> {
11 let pool = PgPool::connect(database_url).await?;
12 sqlx::migrate!("./src/migrations").run(&pool).await?;
13 Ok(Self { pool })
14 }
15}
16
17#[async_trait]
18impl StorageBackend for PgStorage {
19 async fn get(&self, path: &str) -> StorageResult<Option<StorageEntry>> {
20 let row = sqlx::query_as::<_, (Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)>(
21 "SELECT value, expires_at FROM kv_store WHERE path = $1",
22 )
23 .bind(path)
24 .fetch_optional(&self.pool)
25 .await
26 .map_err(|e| StorageError::Backend(e.to_string()))?;
27
28 Ok(row.map(|(value, expires_at)| StorageEntry { value, expires_at }))
29 }
30
31 async fn put(&self, path: &str, entry: StorageEntry) -> StorageResult<()> {
32 sqlx::query(
33 "INSERT INTO kv_store (path, value, expires_at, updated_at)
34 VALUES ($1, $2, $3, now())
35 ON CONFLICT (path) DO UPDATE SET value = $2, expires_at = $3, updated_at = now()",
36 )
37 .bind(path)
38 .bind(entry.value)
39 .bind(entry.expires_at)
40 .execute(&self.pool)
41 .await
42 .map_err(|e| StorageError::Backend(e.to_string()))?;
43 Ok(())
44 }
45
46 async fn delete(&self, path: &str) -> StorageResult<()> {
47 sqlx::query("DELETE FROM kv_store WHERE path = $1")
48 .bind(path)
49 .execute(&self.pool)
50 .await
51 .map_err(|e| StorageError::Backend(e.to_string()))?;
52 Ok(())
53 }
54
55 async fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
56 let rows: Vec<(String,)> =
57 sqlx::query_as("SELECT path FROM kv_store WHERE path LIKE $1 || '%'")
58 .bind(prefix)
59 .fetch_all(&self.pool)
60 .await
61 .map_err(|e| StorageError::Backend(e.to_string()))?;
62 Ok(rows.into_iter().map(|(p,)| p).collect())
63 }
64}