Skip to main content

graphmind_sdk/
remote.rs

1//! RemoteClient — network client for a running Graphmind server
2//!
3//! Connects via HTTP to the Graphmind HTTP API.
4
5use async_trait::async_trait;
6use reqwest::Client;
7
8use crate::client::GraphmindClient;
9use crate::error::{GraphmindError, GraphmindResult};
10use crate::models::{QueryResult, ServerStatus};
11
12/// Network client that connects to a running Graphmind server.
13///
14/// Uses HTTP transport for `/api/query` and `/api/status` endpoints.
15pub struct RemoteClient {
16    http_base_url: String,
17    http_client: Client,
18}
19
20impl RemoteClient {
21    /// Create a new RemoteClient connecting to the given HTTP base URL.
22    ///
23    /// # Example
24    /// ```no_run
25    /// # use graphmind_sdk::RemoteClient;
26    /// let client = RemoteClient::new("http://localhost:8080");
27    /// ```
28    pub fn new(http_base_url: &str) -> Self {
29        Self {
30            http_base_url: http_base_url.trim_end_matches('/').to_string(),
31            http_client: Client::new(),
32        }
33    }
34
35    /// Execute a POST request to /api/query
36    async fn post_query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
37        let url = format!("{}/api/query", self.http_base_url);
38        let body = serde_json::json!({ "query": cypher, "graph": graph });
39
40        let response = self.http_client.post(&url).json(&body).send().await?;
41
42        if response.status().is_success() {
43            let result: QueryResult = response.json().await?;
44            Ok(result)
45        } else {
46            let error_body: serde_json::Value = response
47                .json()
48                .await
49                .unwrap_or_else(|_| serde_json::json!({"error": "Unknown error"}));
50            let msg = error_body
51                .get("error")
52                .and_then(|v| v.as_str())
53                .unwrap_or("Unknown error")
54                .to_string();
55            Err(GraphmindError::QueryError(msg))
56        }
57    }
58}
59
60#[async_trait]
61impl GraphmindClient for RemoteClient {
62    async fn query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
63        self.post_query(graph, cypher).await
64    }
65
66    async fn query_readonly(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
67        self.post_query(graph, cypher).await
68    }
69
70    async fn delete_graph(&self, graph: &str) -> GraphmindResult<()> {
71        // The HTTP API doesn't expose GRAPH.DELETE directly.
72        // We can execute a Cypher that deletes all nodes/edges.
73        self.post_query(graph, "MATCH (n) DELETE n").await?;
74        Ok(())
75    }
76
77    async fn list_graphs(&self) -> GraphmindResult<Vec<String>> {
78        // Single-graph mode in OSS
79        Ok(vec!["default".to_string()])
80    }
81
82    async fn status(&self) -> GraphmindResult<ServerStatus> {
83        let url = format!("{}/api/status", self.http_base_url);
84        let response = self.http_client.get(&url).send().await?;
85
86        if response.status().is_success() {
87            let status: ServerStatus = response.json().await?;
88            Ok(status)
89        } else {
90            Err(GraphmindError::ConnectionError(format!(
91                "Status endpoint returned {}",
92                response.status()
93            )))
94        }
95    }
96
97    async fn ping(&self) -> GraphmindResult<String> {
98        let status = self.status().await?;
99        if status.status == "healthy" {
100            Ok("PONG".to_string())
101        } else {
102            Err(GraphmindError::ConnectionError(format!(
103                "Server unhealthy: {}",
104                status.status
105            )))
106        }
107    }
108
109    async fn schema(&self, _graph: &str) -> GraphmindResult<String> {
110        let url = format!("{}/api/schema", self.http_base_url);
111        let response = self.http_client.get(&url).send().await?;
112
113        if response.status().is_success() {
114            let body: serde_json::Value = response.json().await?;
115            Ok(serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string()))
116        } else {
117            Err(GraphmindError::ConnectionError(format!(
118                "Schema endpoint returned {}",
119                response.status()
120            )))
121        }
122    }
123
124    async fn explain(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
125        let prefixed = if cypher.trim().to_uppercase().starts_with("EXPLAIN") {
126            cypher.to_string()
127        } else {
128            format!("EXPLAIN {}", cypher)
129        };
130        self.post_query(graph, &prefixed).await
131    }
132
133    async fn profile(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
134        let prefixed = if cypher.trim().to_uppercase().starts_with("PROFILE") {
135            cypher.to_string()
136        } else {
137            format!("PROFILE {}", cypher)
138        };
139        self.post_query(graph, &prefixed).await
140    }
141}