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 std::collections::HashMap;
9
10use crate::client::GraphmindClient;
11use crate::error::{GraphmindError, GraphmindResult};
12use crate::models::{QueryResult, ServerStatus};
13
14/// Network client that connects to a running Graphmind server.
15///
16/// Uses HTTP transport for `/api/query` and `/api/status` endpoints.
17pub struct RemoteClient {
18    http_base_url: String,
19    http_client: Client,
20}
21
22impl RemoteClient {
23    /// Create a new RemoteClient connecting to the given HTTP base URL.
24    ///
25    /// # Example
26    /// ```no_run
27    /// # use graphmind_sdk::RemoteClient;
28    /// let client = RemoteClient::new("http://localhost:8080");
29    /// ```
30    pub fn new(http_base_url: &str) -> Self {
31        Self {
32            http_base_url: http_base_url.trim_end_matches('/').to_string(),
33            http_client: Client::new(),
34        }
35    }
36
37    /// Execute a POST request to /api/query
38    async fn post_query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
39        self.post_query_with_params(graph, cypher, None).await
40    }
41
42    /// Execute a POST request to /api/query with optional parameters
43    async fn post_query_with_params(
44        &self,
45        graph: &str,
46        cypher: &str,
47        params: Option<HashMap<String, serde_json::Value>>,
48    ) -> GraphmindResult<QueryResult> {
49        let url = format!("{}/api/query", self.http_base_url);
50        let mut body = serde_json::json!({ "query": cypher, "graph": graph });
51        if let Some(p) = params {
52            body["params"] = serde_json::to_value(p)
53                .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
54        }
55
56        let response = self.http_client.post(&url).json(&body).send().await?;
57
58        if response.status().is_success() {
59            let result: QueryResult = response.json().await?;
60            Ok(result)
61        } else {
62            let error_body: serde_json::Value = response
63                .json()
64                .await
65                .unwrap_or_else(|_| serde_json::json!({"error": "Unknown error"}));
66            let msg = error_body
67                .get("error")
68                .and_then(|v| v.as_str())
69                .unwrap_or("Unknown error")
70                .to_string();
71            Err(GraphmindError::QueryError(msg))
72        }
73    }
74}
75
76#[async_trait]
77impl GraphmindClient for RemoteClient {
78    async fn query(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
79        self.post_query(graph, cypher).await
80    }
81
82    async fn query_with_params(
83        &self,
84        graph: &str,
85        cypher: &str,
86        params: HashMap<String, serde_json::Value>,
87    ) -> GraphmindResult<QueryResult> {
88        self.post_query_with_params(graph, cypher, Some(params))
89            .await
90    }
91
92    async fn query_readonly(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
93        self.post_query(graph, cypher).await
94    }
95
96    async fn delete_graph(&self, graph: &str) -> GraphmindResult<()> {
97        // First detach-delete all nodes and edges
98        let _ = self.post_query(graph, "MATCH (n) DETACH DELETE n").await;
99        // Then remove the graph from the tenant store via the API
100        let url = format!("{}/api/graphs/{}", self.http_base_url, graph);
101        let _ = self.http_client.delete(&url).send().await;
102        Ok(())
103    }
104
105    async fn list_graphs(&self) -> GraphmindResult<Vec<String>> {
106        // Single-graph mode in OSS
107        Ok(vec!["default".to_string()])
108    }
109
110    async fn status(&self, graph: &str) -> GraphmindResult<ServerStatus> {
111        let url = format!("{}/api/status?graph={}", self.http_base_url, graph);
112        let response = self.http_client.get(&url).send().await?;
113
114        if response.status().is_success() {
115            let status: ServerStatus = response.json().await?;
116            Ok(status)
117        } else {
118            Err(GraphmindError::ConnectionError(format!(
119                "Status endpoint returned {}",
120                response.status()
121            )))
122        }
123    }
124
125    async fn ping(&self) -> GraphmindResult<String> {
126        let status = self.status("default").await?;
127        if status.status == "healthy" {
128            Ok("PONG".to_string())
129        } else {
130            Err(GraphmindError::ConnectionError(format!(
131                "Server unhealthy: {}",
132                status.status
133            )))
134        }
135    }
136
137    async fn schema(&self, graph: &str) -> GraphmindResult<String> {
138        let url = format!("{}/api/schema?graph={}", self.http_base_url, graph);
139        let response = self.http_client.get(&url).send().await?;
140
141        if response.status().is_success() {
142            let body: serde_json::Value = response.json().await?;
143            Ok(serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string()))
144        } else {
145            Err(GraphmindError::ConnectionError(format!(
146                "Schema endpoint returned {}",
147                response.status()
148            )))
149        }
150    }
151
152    async fn explain(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
153        let prefixed = if cypher.trim().to_uppercase().starts_with("EXPLAIN") {
154            cypher.to_string()
155        } else {
156            format!("EXPLAIN {}", cypher)
157        };
158        self.post_query(graph, &prefixed).await
159    }
160
161    async fn profile(&self, graph: &str, cypher: &str) -> GraphmindResult<QueryResult> {
162        let prefixed = if cypher.trim().to_uppercase().starts_with("PROFILE") {
163            cypher.to_string()
164        } else {
165            format!("PROFILE {}", cypher)
166        };
167        self.post_query(graph, &prefixed).await
168    }
169}