use redact::Secret; use sqlx::query_as; use url::Url; use uuid::Uuid; use crate::{client::AppDbClient, macros::with_id_query}; /// Represents a Postgres cluster to be used as the backing database for zero /// or more workspaces. At this time, rows in the `clusters` table are created /// manually by an administrator rather than through the web application. #[derive(Clone, Debug)] pub struct Cluster { /// Primary key (defaults to UUIDv7). pub id: Uuid, /// Host address, including port specifier if not ":5432". pub host: String, /// Username of the root Phonograph role for this cluster. Defaults to /// "phono". pub username: String, // TODO: encrypt passwords /// Password of the root Phonograph role for this cluster. pub password: Secret, } with_id_query!(Cluster, sql = "select * from clusters where id = $1"); impl Cluster { /// Construct an authenticated postgresql:// connection URL for a specific /// database in this cluster. URL is wrapped in a [`redact::Secret`] because /// it contains a plaintext password. pub fn conn_str_for_db( &self, db_name: &str, auth_override: Option<(&str, Secret<&str>)>, ) -> Result, ConnStrError> { let (username, password) = auth_override.unwrap_or(( self.username.as_str(), Secret::new(self.password.expose_secret().as_str()), )); let mut url = Url::parse(&format!("postgresql://{host}", host = self.host))?; url.set_path(db_name); url.set_username(username) .map_err(|_| ConnStrError::Build { context: "username", })?; url.set_password(Some(password.expose_secret())) .map_err(|_| ConnStrError::Build { context: "password", })?; Ok(Secret::new(url)) } /// For use only in single-cluster setups. If exactly one row is present, it /// is fetched and returned. Otherwise, returns an error. pub async fn fetch_only(app_db: &mut AppDbClient) -> sqlx::Result { let mut rows = query_as!(Self, "select * from clusters limit 2") .fetch_all(app_db.get_conn()) .await?; if rows.len() == 1 { Ok(rows.pop().expect("just checked that vec len == 1")) } else { Err(sqlx::Error::RowNotFound) } } } #[derive(Clone, Copy, Debug, thiserror::Error)] #[error("error building postgresql:// connection string")] pub enum ConnStrError { Parse(url::ParseError), #[error("error building postgresql:// connection string: {context}")] Build { context: &'static str, }, } impl From for ConnStrError { fn from(value: url::ParseError) -> Self { Self::Parse(value) } }