Skip to main content

reddb_client_connector/
lib.rs

1//! Workspace-internal gRPC connector.
2//!
3//! Thin wrapper around the generated tonic `RedDbClient<Channel>`
4//! that adds bearer-token auth metadata + ergonomic typed
5//! responses. Lives in its own crate so both `reddb-server`
6//! (`rpc_stdio` mode) and `reddb-client` (the published driver)
7//! can reach it without setting up a circular path dependency
8//! through the `reddb` umbrella.
9//!
10//! Engine-free by design: only `tonic` + `reddb-grpc-proto` deps.
11//! See `crates/reddb-client-connector/Cargo.toml` for rationale.
12
13use reddb_grpc_proto::red_db_client::RedDbClient;
14use reddb_grpc_proto::*;
15use tonic::transport::Channel;
16use tonic::Request;
17
18#[derive(Debug, Clone)]
19pub struct HealthStatus {
20    pub healthy: bool,
21    pub state: String,
22    pub checked_at_unix_ms: u64,
23}
24
25#[derive(Debug, Clone)]
26pub struct QueryResponse {
27    pub ok: bool,
28    pub mode: String,
29    pub statement: String,
30    pub engine: String,
31    pub columns: Vec<String>,
32    pub record_count: u64,
33    /// Rows changed by a DML statement (INSERT / UPDATE / DELETE). 0 for
34    /// reads. Distinct from `record_count` (rows returned).
35    pub affected_rows: u64,
36    pub result_json: String,
37}
38
39#[derive(Debug, Clone)]
40pub struct CreatedEntity {
41    pub ok: bool,
42    pub id: u64,
43    pub entity_json: String,
44}
45
46#[derive(Debug, Clone)]
47pub struct BulkCreateStatus {
48    pub ok: bool,
49    pub count: u64,
50    pub ids: Vec<u64>,
51}
52
53#[derive(Debug, Clone)]
54pub struct OperationStatus {
55    pub ok: bool,
56    pub message: String,
57}
58
59#[derive(Clone)]
60pub struct RedDBClient {
61    inner: RedDbClient<Channel>,
62    token: Option<String>,
63    pub addr: String,
64}
65
66impl RedDBClient {
67    pub async fn connect(
68        addr: &str,
69        token: Option<String>,
70    ) -> Result<Self, Box<dyn std::error::Error>> {
71        let endpoint = if addr.starts_with("http") {
72            addr.to_string()
73        } else {
74            format!("http://{}", addr)
75        };
76        let inner = RedDbClient::connect(endpoint.clone()).await?;
77        Ok(Self {
78            inner,
79            token,
80            addr: endpoint,
81        })
82    }
83
84    fn auth_request<T>(&self, inner: T) -> Request<T> {
85        let mut req = Request::new(inner);
86        if let Some(ref token) = self.token {
87            if let Ok(value) = format!("Bearer {}", token).parse() {
88                req.metadata_mut().insert("authorization", value);
89            }
90        }
91        req
92    }
93
94    /// Update the auth token (e.g. after a successful login).
95    pub fn set_token(&mut self, token: String) {
96        self.token = Some(token);
97    }
98
99    pub async fn health_status(&mut self) -> Result<HealthStatus, Box<dyn std::error::Error>> {
100        let req = self.auth_request(Empty {});
101        let resp = self.inner.health(req).await?;
102        let reply = resp.into_inner();
103        Ok(HealthStatus {
104            healthy: reply.healthy,
105            state: reply.state,
106            checked_at_unix_ms: reply.checked_at_unix_ms,
107        })
108    }
109
110    pub async fn health(&mut self) -> Result<String, Box<dyn std::error::Error>> {
111        let reply = self.health_status().await?;
112        Ok(format!(
113            "state: {}, healthy: {}",
114            reply.state, reply.healthy
115        ))
116    }
117
118    pub async fn query_reply(
119        &mut self,
120        sql: &str,
121    ) -> Result<QueryResponse, Box<dyn std::error::Error>> {
122        self.query_reply_with_params(sql, Vec::new()).await
123    }
124
125    pub async fn query_reply_with_params(
126        &mut self,
127        sql: &str,
128        params: Vec<QueryValue>,
129    ) -> Result<QueryResponse, Box<dyn std::error::Error>> {
130        let req = self.auth_request(QueryRequest {
131            query: sql.to_string(),
132            entity_types: vec![],
133            capabilities: vec![],
134            params,
135        });
136        let resp = self.inner.query(req).await?;
137        let reply = resp.into_inner();
138        Ok(QueryResponse {
139            ok: reply.ok,
140            mode: reply.mode,
141            statement: reply.statement,
142            engine: reply.engine,
143            columns: reply.columns,
144            record_count: reply.record_count,
145            affected_rows: reply.affected_rows,
146            result_json: reply.result_json,
147        })
148    }
149
150    pub async fn query(&mut self, sql: &str) -> Result<String, Box<dyn std::error::Error>> {
151        let reply = self.query_reply(sql).await?;
152        Ok(reply.result_json)
153    }
154
155    pub async fn collections(&mut self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
156        let req = self.auth_request(Empty {});
157        let resp = self.inner.collections(req).await?;
158        Ok(resp.into_inner().collections)
159    }
160
161    pub async fn scan(
162        &mut self,
163        collection: &str,
164        limit: u64,
165    ) -> Result<String, Box<dyn std::error::Error>> {
166        let req = self.auth_request(ScanRequest {
167            collection: collection.to_string(),
168            offset: 0,
169            limit,
170        });
171        let resp = self.inner.scan(req).await?;
172        let reply = resp.into_inner();
173        let items: Vec<String> = reply.items.iter().map(|e| e.json.clone()).collect();
174        Ok(format!(
175            "total: {}, items: [{}]",
176            reply.total,
177            items.join(", ")
178        ))
179    }
180
181    pub async fn stats(&mut self) -> Result<String, Box<dyn std::error::Error>> {
182        let req = self.auth_request(Empty {});
183        let resp = self.inner.stats(req).await?;
184        let reply = resp.into_inner();
185        Ok(format!(
186            "collections: {}, entities: {}, memory: {} bytes, started_at: {}",
187            reply.collection_count,
188            reply.total_entities,
189            reply.total_memory_bytes,
190            reply.started_at_unix_ms
191        ))
192    }
193
194    pub async fn create_row(
195        &mut self,
196        collection: &str,
197        json: &str,
198    ) -> Result<String, Box<dyn std::error::Error>> {
199        let reply = self.create_row_entity(collection, json).await?;
200        Ok(format!("id: {}, entity: {}", reply.id, reply.entity_json))
201    }
202
203    pub async fn create_row_entity(
204        &mut self,
205        collection: &str,
206        json: &str,
207    ) -> Result<CreatedEntity, Box<dyn std::error::Error>> {
208        let req = self.auth_request(JsonCreateRequest {
209            collection: collection.to_string(),
210            payload_json: json.to_string(),
211        });
212        let resp = self.inner.create_row(req).await?;
213        let reply = resp.into_inner();
214        Ok(CreatedEntity {
215            ok: reply.ok,
216            id: reply.id,
217            entity_json: reply.entity_json,
218        })
219    }
220
221    pub async fn bulk_create_rows(
222        &mut self,
223        collection: &str,
224        payload_json: Vec<String>,
225    ) -> Result<BulkCreateStatus, Box<dyn std::error::Error>> {
226        let req = self.auth_request(JsonBulkCreateRequest {
227            collection: collection.to_string(),
228            payload_json,
229        });
230        let resp = self.inner.bulk_create_rows(req).await?;
231        let reply = resp.into_inner();
232        Ok(BulkCreateStatus {
233            ok: reply.ok,
234            count: reply.count,
235            ids: reply.items.into_iter().map(|item| item.id).collect(),
236        })
237    }
238
239    pub async fn explain(&mut self, sql: &str) -> Result<String, Box<dyn std::error::Error>> {
240        let req = self.auth_request(QueryRequest {
241            query: sql.to_string(),
242            entity_types: vec![],
243            capabilities: vec![],
244            params: vec![],
245        });
246        let resp = self.inner.explain_query(req).await?;
247        Ok(resp.into_inner().payload)
248    }
249
250    pub async fn login(
251        &mut self,
252        username: &str,
253        password: &str,
254    ) -> Result<String, Box<dyn std::error::Error>> {
255        let payload = format!(
256            "{{\"username\":\"{}\",\"password\":\"{}\"}}",
257            username, password
258        );
259        let req = self.auth_request(JsonPayloadRequest {
260            payload_json: payload,
261        });
262        let resp = self.inner.auth_login(req).await?;
263        let reply = resp.into_inner();
264        Ok(reply.payload)
265    }
266
267    pub async fn replication_status(&mut self) -> Result<String, Box<dyn std::error::Error>> {
268        let req = self.auth_request(Empty {});
269        let resp = self.inner.replication_status(req).await?;
270        Ok(resp.into_inner().payload)
271    }
272
273    /// Fetch the canonical `Topology` payload (issue #167 / ADR 0008).
274    /// Returns the raw `topology_bytes` so the caller can hand them
275    /// straight to `TopologyConsumer::consume_bytes`. Engine-free —
276    /// this connector knows nothing about the wire schema beyond the
277    /// proto envelope.
278    pub async fn topology(&mut self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
279        let req = self.auth_request(TopologyRequest {});
280        let resp = self.inner.topology(req).await?;
281        Ok(resp.into_inner().topology_bytes)
282    }
283
284    pub async fn delete_entity(
285        &mut self,
286        collection: &str,
287        id: u64,
288    ) -> Result<OperationStatus, Box<dyn std::error::Error>> {
289        let req = self.auth_request(DeleteEntityRequest {
290            collection: collection.to_string(),
291            id,
292        });
293        let resp = self.inner.delete_entity(req).await?;
294        let reply = resp.into_inner();
295        Ok(OperationStatus {
296            ok: reply.ok,
297            message: reply.message,
298        })
299    }
300}