clnrm_core/services/
surrealdb.rs

1//! SurrealDB service plugin
2//!
3//! Production-ready SurrealDB container management with health checks
4//! and connection verification.
5
6use crate::cleanroom::{HealthStatus, ServiceHandle, ServicePlugin};
7use crate::error::{CleanroomError, Result};
8use std::collections::HashMap;
9use std::sync::Arc;
10use surrealdb::{
11    engine::remote::ws::{Client, Ws},
12    opt::auth::Root,
13    Surreal,
14};
15use testcontainers::runners::AsyncRunner;
16use testcontainers_modules::surrealdb::{SurrealDb, SURREALDB_PORT};
17use tokio::sync::RwLock;
18use uuid::Uuid;
19
20#[derive(Debug)]
21pub struct SurrealDbPlugin {
22    name: String,
23    container_id: Arc<RwLock<Option<String>>>,
24    username: String,
25    password: String,
26    strict: bool,
27}
28
29impl Default for SurrealDbPlugin {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl SurrealDbPlugin {
36    pub fn new() -> Self {
37        Self::with_credentials("root", "root")
38    }
39
40    pub fn with_credentials(username: &str, password: &str) -> Self {
41        Self {
42            name: "surrealdb".to_string(),
43            container_id: Arc::new(RwLock::new(None)),
44            username: username.to_string(),
45            password: password.to_string(),
46            strict: false,
47        }
48    }
49
50    pub fn with_name(mut self, name: &str) -> Self {
51        self.name = name.to_string();
52        self
53    }
54
55    pub fn with_strict(mut self, strict: bool) -> Self {
56        self.strict = strict;
57        self
58    }
59
60    async fn verify_connection(&self, host_port: u16) -> Result<()> {
61        let url = format!("127.0.0.1:{}", host_port);
62        let db: Surreal<Client> = Surreal::init();
63
64        db.connect::<Ws>(url).await.map_err(|e| {
65            CleanroomError::connection_failed("Failed to connect to SurrealDB")
66                .with_source(e.to_string())
67        })?;
68
69        db.signin(Root {
70            username: &self.username,
71            password: &self.password,
72        })
73        .await
74        .map_err(|e| {
75            CleanroomError::service_error("Failed to authenticate").with_source(e.to_string())
76        })?;
77
78        Ok(())
79    }
80}
81
82impl ServicePlugin for SurrealDbPlugin {
83    fn name(&self) -> &str {
84        &self.name
85    }
86
87    fn start(&self) -> Result<ServiceHandle> {
88        // Use tokio::task::block_in_place for async operations
89        tokio::task::block_in_place(|| {
90            tokio::runtime::Handle::current().block_on(async {
91                let db_config = SurrealDb::default()
92                    .with_user(&self.username)
93                    .with_password(&self.password)
94                    .with_strict(self.strict)
95                    .with_all_capabilities(true);
96
97                let node = db_config.start().await.map_err(|e| {
98                    CleanroomError::container_error("Failed to start SurrealDB container")
99                        .with_context("Container startup failed")
100                        .with_source(e.to_string())
101                })?;
102
103                let host_port = node.get_host_port_ipv4(SURREALDB_PORT).await.map_err(|e| {
104                    CleanroomError::container_error("Failed to get container port")
105                        .with_source(e.to_string())
106                })?;
107
108                // Verify connection works
109                self.verify_connection(host_port).await?;
110
111                let mut container_guard = self.container_id.write().await;
112                *container_guard = Some(format!("container-{}", host_port));
113
114                let mut metadata = HashMap::new();
115                metadata.insert("host".to_string(), "127.0.0.1".to_string());
116                metadata.insert("port".to_string(), host_port.to_string());
117                metadata.insert("username".to_string(), self.username.clone());
118                metadata.insert("database_type".to_string(), "surrealdb".to_string());
119                metadata.insert(
120                    "connection_string".to_string(),
121                    format!("ws://127.0.0.1:{}", host_port),
122                );
123
124                Ok(ServiceHandle {
125                    id: Uuid::new_v4().to_string(),
126                    service_name: self.name.clone(),
127                    metadata,
128                })
129            })
130        })
131    }
132
133    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
134        // Use tokio::task::block_in_place for async operations
135        tokio::task::block_in_place(|| {
136            tokio::runtime::Handle::current().block_on(async {
137                let mut container_guard = self.container_id.write().await;
138                if container_guard.is_some() {
139                    *container_guard = None; // Drop triggers container cleanup
140                }
141                Ok(())
142            })
143        })
144    }
145
146    fn health_check(&self, handle: &ServiceHandle) -> HealthStatus {
147        if handle.metadata.contains_key("port") && handle.metadata.contains_key("connection_string")
148        {
149            HealthStatus::Healthy
150        } else {
151            HealthStatus::Unknown
152        }
153    }
154}