secrets-storage-postgres 0.1.1

PostgreSQL-backed StorageBackend implementation for the secrets server.
Documentation
use async_trait::async_trait;
use secrets_core::storage::{StorageBackend, StorageEntry, StorageError, StorageResult};
use sqlx::PgPool;

pub struct PgStorage {
    pool: PgPool,
}

impl PgStorage {
    pub async fn connect(database_url: &str) -> Result<Self, sqlx::Error> {
        let pool = PgPool::connect(database_url).await?;
        sqlx::migrate!("./src/migrations").run(&pool).await?;
        Ok(Self { pool })
    }
}

#[async_trait]
impl StorageBackend for PgStorage {
    async fn get(&self, path: &str) -> StorageResult<Option<StorageEntry>> {
        let row = sqlx::query_as::<_, (Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)>(
            "SELECT value, expires_at FROM kv_store WHERE path = $1",
        )
        .bind(path)
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| StorageError::Backend(e.to_string()))?;

        Ok(row.map(|(value, expires_at)| StorageEntry { value, expires_at }))
    }

    async fn put(&self, path: &str, entry: StorageEntry) -> StorageResult<()> {
        sqlx::query(
            "INSERT INTO kv_store (path, value, expires_at, updated_at)
             VALUES ($1, $2, $3, now())
             ON CONFLICT (path) DO UPDATE SET value = $2, expires_at = $3, updated_at = now()",
        )
        .bind(path)
        .bind(entry.value)
        .bind(entry.expires_at)
        .execute(&self.pool)
        .await
        .map_err(|e| StorageError::Backend(e.to_string()))?;
        Ok(())
    }

    async fn delete(&self, path: &str) -> StorageResult<()> {
        sqlx::query("DELETE FROM kv_store WHERE path = $1")
            .bind(path)
            .execute(&self.pool)
            .await
            .map_err(|e| StorageError::Backend(e.to_string()))?;
        Ok(())
    }

    async fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
        let rows: Vec<(String,)> =
            sqlx::query_as("SELECT path FROM kv_store WHERE path LIKE $1 || '%'")
                .bind(prefix)
                .fetch_all(&self.pool)
                .await
                .map_err(|e| StorageError::Backend(e.to_string()))?;
        Ok(rows.into_iter().map(|(p,)| p).collect())
    }
}