lmrc_postgres/
adapter.rs

1//! # PostgreSQL Adapter
2//!
3//! Adapter implementation that wraps PostgreSQL user/database management functions
4//! and implements the `DatabaseProvider` port trait from `lmrc-ports`.
5//!
6//! This adapter allows PostgreSQL to be used interchangeably with other database
7//! providers in the LMRC Stack hexagonal architecture.
8
9use async_trait::async_trait;
10use crate::{Error as PostgresError, Privilege};
11use lmrc_ports::{
12    CreatedDatabase, DatabaseCreateRequest, DatabaseProvider, DatabaseUser, DatabaseUserRequest,
13    PortError, PortResult,
14};
15use lmrc_ssh::{AuthMethod, SshClient};
16
17/// PostgreSQL adapter implementing the DatabaseProvider port
18///
19/// This adapter is stateless and accepts connection details via request parameters.
20pub struct PostgresAdapter;
21
22impl PostgresAdapter {
23    /// Create a new PostgreSQL adapter
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Create SSH client for database operations
29    ///
30    /// # Arguments
31    ///
32    /// * `host` - Database server host/IP
33    /// * `ssh_key_path` - SSH private key path for server access
34    async fn create_ssh_client(&self, host: &str, ssh_key_path: &str) -> PortResult<SshClient> {
35        let ssh_auth = AuthMethod::PublicKey {
36            username: "root".to_string(),
37            private_key_path: ssh_key_path.to_string(),
38            passphrase: None,
39        };
40
41        let client = SshClient::new(host, 22)
42            .map_err(|e| PortError::NetworkError(format!("Failed to create SSH client: {}", e)))?
43            .with_auth(ssh_auth)
44            .connect()
45            .map_err(|e| {
46                PortError::NetworkError(format!("Failed to connect via SSH: {}", e))
47            })?;
48
49        Ok(client)
50    }
51}
52
53/// Convert PostgreSQL error to PortError
54fn convert_error(err: PostgresError) -> PortError {
55    match err {
56        PostgresError::Ssh(e) => PortError::NetworkError(format!("SSH error: {}", e)),
57        PostgresError::SshExecution { message, command } => PortError::OperationFailed(format!(
58            "SSH command '{}' failed: {}",
59            command, message
60        )),
61        PostgresError::Installation(msg) => PortError::OperationFailed(format!("Installation failed: {}", msg)),
62        PostgresError::Configuration(msg) => PortError::InvalidConfiguration(msg),
63        PostgresError::NotInstalled => PortError::OperationFailed("PostgreSQL is not installed".to_string()),
64        PostgresError::AlreadyInstalled(version) => PortError::AlreadyExists {
65            resource_type: "PostgreSQL installation".to_string(),
66            resource_id: version,
67        },
68        PostgresError::InvalidVersion(version) => {
69            PortError::InvalidConfiguration(format!("Invalid PostgreSQL version: {}", version))
70        }
71        PostgresError::InvalidConfig { parameter, value } => {
72            PortError::InvalidConfiguration(format!("Invalid config {} = {}", parameter, value))
73        }
74        PostgresError::MissingConfig(msg) => PortError::InvalidConfiguration(format!("Missing config: {}", msg)),
75        PostgresError::ServiceError(msg) => PortError::OperationFailed(format!("Service error: {}", msg)),
76        PostgresError::ConnectionTest(msg) => {
77            PortError::OperationFailed(format!("Connection test failed: {}", msg))
78        }
79        PostgresError::Uninstallation(msg) => {
80            PortError::OperationFailed(format!("Uninstallation failed: {}", msg))
81        }
82        PostgresError::Io(e) => PortError::OperationFailed(format!("IO error: {}", e)),
83        PostgresError::Serialization(e) => PortError::OperationFailed(format!("Serialization error: {}", e)),
84        PostgresError::Other(msg) => PortError::OperationFailed(msg),
85    }
86}
87
88#[async_trait]
89impl DatabaseProvider for PostgresAdapter {
90    async fn create_database(&self, request: DatabaseCreateRequest) -> PortResult<CreatedDatabase> {
91        let mut ssh = self.create_ssh_client(&request.host, &request.ssh_key_path).await?;
92
93        // Create database with options
94        // Signature: (ssh, database_name, owner, encoding, template)
95        crate::create_database_with_options(
96            &mut ssh,
97            &request.name,
98            Some(&request.owner),
99            request.encoding.as_deref(),
100            None, // template
101        )
102        .await
103        .map_err(convert_error)?;
104
105        // Build connection string
106        let connection_string = format!(
107            "postgresql://{}@{}:{}/{}",
108            request.owner, request.host, request.port, request.name
109        );
110
111        Ok(CreatedDatabase {
112            name: request.name.clone(),
113            owner: request.owner.clone(),
114            connection_string,
115            host: request.host,
116            port: request.port,
117        })
118    }
119
120    async fn drop_database(&self, name: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
121        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;
122
123        crate::drop_database(&mut ssh, name)
124            .await
125            .map_err(convert_error)?;
126
127        Ok(())
128    }
129
130    async fn list_databases(&self, host: &str, port: u16, ssh_key_path: &str) -> PortResult<Vec<String>> {
131        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;
132
133        let databases = crate::list_databases(&mut ssh)
134            .await
135            .map_err(convert_error)?;
136
137        Ok(databases.into_iter().map(|db| db.name).collect())
138    }
139
140    async fn database_exists(&self, name: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<bool> {
141        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;
142
143        crate::database_exists(&mut ssh, name)
144            .await
145            .map_err(convert_error)
146    }
147
148    async fn create_user(&self, request: DatabaseUserRequest) -> PortResult<DatabaseUser> {
149        let mut ssh = self.create_ssh_client(&request.host, &request.ssh_key_path).await?;
150
151        // Create user with options
152        crate::create_user_with_options(
153            &mut ssh,
154            &request.username,
155            &request.password,
156            request.superuser,
157            false, // create_db
158            false, // create_role
159            Some(-1),    // connection_limit (-1 = unlimited)
160        )
161        .await
162        .map_err(convert_error)?;
163
164        Ok(DatabaseUser {
165            username: request.username,
166            superuser: request.superuser,
167        })
168    }
169
170    async fn drop_user(&self, username: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
171        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;
172
173        crate::drop_user(&mut ssh, username)
174            .await
175            .map_err(convert_error)?;
176
177        Ok(())
178    }
179
180    async fn grant_privileges(&self, database: &str, username: &str, host: &str, port: u16, ssh_key_path: &str) -> PortResult<()> {
181        let mut ssh = self.create_ssh_client(host, ssh_key_path).await?;
182
183        // Grant all privileges on the database
184        // Signature: (ssh, database, username, privileges)
185        crate::grant_privileges(
186            &mut ssh,
187            database,
188            username,
189            &[Privilege::All],
190        )
191        .await
192        .map_err(convert_error)?;
193
194        Ok(())
195    }
196}