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