clnrm_core/services/
surrealdb.rs1use 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 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 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 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; }
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}