use std::fmt;
use ironflow_core::error::OperationError;
use ironflow_core::operation::OperationContext;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
pub struct PostgresClient {
pool: PgPool,
}
impl PostgresClient {
pub async fn connect(url: &str) -> Result<Self, OperationError> {
if url.is_empty() {
return Err(OperationError::External {
origin: "postgres".to_string(),
message: "connection URL must not be empty".to_string(),
});
}
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(url)
.await
.map_err(|e| OperationError::External {
origin: "postgres".to_string(),
message: e.to_string(),
})?;
Ok(Self { pool })
}
pub fn from_pool(pool: PgPool) -> Self {
Self { pool }
}
pub async fn from_context(ctx: &OperationContext) -> Result<Self, OperationError> {
let secret = ctx.secrets().get("postgres_url").await?;
let url = secret
.ok_or_else(|| OperationError::External {
origin: "postgres".to_string(),
message: "secret 'postgres_url' not found".to_string(),
})?
.value;
Self::connect(&url).await
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
}
impl fmt::Debug for PostgresClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PostgresClient")
.field("url", &"[REDACTED]")
.finish()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use ironflow_core::operation::{NoopSecretResolver, OperationContext};
use super::*;
#[tokio::test]
async fn debug_does_not_leak() {
let client = PostgresClient {
pool: PgPool::connect_lazy("postgres://user:pass@localhost/db").unwrap(),
};
let debug = format!("{client:?}");
assert!(!debug.contains("user"), "leaked user: {debug}");
assert!(!debug.contains("pass"), "leaked password: {debug}");
assert!(!debug.contains("localhost"), "leaked host: {debug}");
assert!(debug.contains("REDACTED"), "missing redaction: {debug}");
}
#[tokio::test]
async fn connect_empty_url() {
let err = PostgresClient::connect("").await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("must not be empty"), "unexpected error: {msg}");
}
#[tokio::test]
async fn from_context_missing_secret() {
let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
let err = PostgresClient::from_context(&ctx).await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("postgres_url"), "unexpected error: {msg}");
}
}