Skip to main content

nexus_sdk/
data.rs

1//! Data operations (nodes and relationships)
2
3use crate::client::NexusClient;
4use crate::error::{NexusError, Result};
5use crate::models::*;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Create node request
10#[derive(Debug, Clone, Serialize)]
11pub struct CreateNodeRequest {
12    /// Node labels
13    pub labels: Vec<String>,
14    /// Node properties
15    #[serde(default)]
16    pub properties: HashMap<String, Value>,
17    /// Optional caller-supplied external id (phase9_external-node-ids).
18    /// Prefixed string form: `sha256:<hex>`, `blake3:<hex>`, `sha512:<hex>`,
19    /// `uuid:<canonical>`, `str:<utf8>`, `bytes:<hex>`.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub external_id: Option<String>,
22    /// Optional conflict policy when `external_id` is set:
23    /// `error` (default), `match`, or `replace`.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub conflict_policy: Option<String>,
26}
27
28/// Create node response
29#[derive(Debug, Clone, Deserialize)]
30pub struct CreateNodeResponse {
31    /// Created node ID
32    pub node_id: u64,
33    /// Success message
34    pub message: String,
35    /// Error message if any
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub error: Option<String>,
38}
39
40/// Get node response
41#[derive(Debug, Clone, Deserialize)]
42pub struct GetNodeResponse {
43    /// Node data
44    pub node: Option<Node>,
45    /// Success message
46    pub message: String,
47    /// Error message if any
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub error: Option<String>,
50}
51
52/// Update node request
53#[derive(Debug, Clone, Serialize)]
54pub struct UpdateNodeRequest {
55    /// Node ID
56    pub node_id: u64,
57    /// New properties (will replace existing)
58    pub properties: HashMap<String, Value>,
59}
60
61/// Update node response
62#[derive(Debug, Clone, Deserialize)]
63pub struct UpdateNodeResponse {
64    /// Success message
65    pub message: String,
66    /// Error message if any
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub error: Option<String>,
69}
70
71/// Delete node response
72#[derive(Debug, Clone, Deserialize)]
73pub struct DeleteNodeResponse {
74    /// Success message
75    pub message: String,
76    /// Error message if any
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub error: Option<String>,
79}
80
81/// Create relationship request
82#[derive(Debug, Clone, Serialize)]
83pub struct CreateRelRequest {
84    /// Source node ID
85    pub source_id: u64,
86    /// Target node ID
87    pub target_id: u64,
88    /// Relationship type
89    pub rel_type: String,
90    /// Relationship properties
91    #[serde(default)]
92    pub properties: HashMap<String, Value>,
93}
94
95/// Create relationship response
96#[derive(Debug, Clone, Deserialize)]
97pub struct CreateRelResponse {
98    /// Relationship ID
99    pub rel_id: u64,
100    /// Success message
101    pub message: String,
102    /// Error message if any
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub error: Option<String>,
105}
106
107/// Update relationship request
108#[derive(Debug, Clone, Serialize)]
109pub struct UpdateRelRequest {
110    /// Relationship ID
111    pub rel_id: u64,
112    /// New properties (will replace existing)
113    pub properties: HashMap<String, Value>,
114}
115
116/// Update relationship response
117#[derive(Debug, Clone, Deserialize)]
118pub struct UpdateRelResponse {
119    /// Success message
120    pub message: String,
121    /// Error message if any
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub error: Option<String>,
124}
125
126/// Delete relationship response
127#[derive(Debug, Clone, Deserialize)]
128pub struct DeleteRelResponse {
129    /// Success message
130    pub message: String,
131    /// Error message if any
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub error: Option<String>,
134}
135
136impl NexusClient {
137    /// Create a new node
138    ///
139    /// # Arguments
140    ///
141    /// * `labels` - Node labels
142    /// * `properties` - Node properties
143    ///
144    /// # Example
145    ///
146    /// ```no_run
147    /// # use nexus_sdk::{NexusClient, Value};
148    /// # use std::collections::HashMap;
149    /// # #[tokio::main]
150    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
151    /// # let client = NexusClient::new("http://localhost:15474")?;
152    /// let mut properties = HashMap::new();
153    /// properties.insert("name".to_string(), Value::String("Alice".to_string()));
154    /// let response = client.create_node(vec!["Person".to_string()], properties).await?;
155    /// tracing::info!("Created node with ID: {}", response.node_id);
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub async fn create_node(
160        &self,
161        labels: Vec<String>,
162        properties: HashMap<String, Value>,
163    ) -> Result<CreateNodeResponse> {
164        let request = CreateNodeRequest {
165            labels,
166            properties,
167            external_id: None,
168            conflict_policy: None,
169        };
170
171        let url = self.get_base_url().join("/data/nodes")?;
172        let mut request_builder = self.get_client().post(url).json(&request);
173
174        request_builder = self.add_auth_headers(request_builder)?;
175
176        let response = self.execute_with_retry(request_builder).await?;
177        let status = response.status();
178
179        if status.is_success() {
180            let result: CreateNodeResponse = response.json().await?;
181            Ok(result)
182        } else {
183            let error_text = response
184                .text()
185                .await
186                .unwrap_or_else(|_| "Unknown error".to_string());
187            Err(NexusError::Api {
188                message: error_text,
189                status: status.as_u16(),
190            })
191        }
192    }
193
194    /// Create a node with a caller-supplied external id.
195    ///
196    /// `external_id` accepts the prefixed string form
197    /// (`sha256:<hex>`, `blake3:<hex>`, `sha512:<hex>`, `uuid:<canonical>`,
198    /// `str:<utf8>`, `bytes:<hex>`). `conflict_policy` is one of
199    /// `"error"` (default), `"match"`, or `"replace"`.
200    ///
201    /// Phase9 §5.5.
202    pub async fn create_node_with_external_id(
203        &self,
204        labels: Vec<String>,
205        properties: HashMap<String, Value>,
206        external_id: impl Into<String>,
207        conflict_policy: Option<&str>,
208    ) -> Result<CreateNodeResponse> {
209        let request = CreateNodeRequest {
210            labels,
211            properties,
212            external_id: Some(external_id.into()),
213            conflict_policy: conflict_policy.map(str::to_owned),
214        };
215
216        let url = self.get_base_url().join("/data/nodes")?;
217        let mut request_builder = self.get_client().post(url).json(&request);
218        request_builder = self.add_auth_headers(request_builder)?;
219        let response = self.execute_with_retry(request_builder).await?;
220        let status = response.status();
221        if status.is_success() {
222            Ok(response.json().await?)
223        } else {
224            let error_text = response
225                .text()
226                .await
227                .unwrap_or_else(|_| "Unknown error".to_string());
228            Err(NexusError::Api {
229                message: error_text,
230                status: status.as_u16(),
231            })
232        }
233    }
234
235    /// Resolve a node by external id (returns `node: None` when absent).
236    ///
237    /// Phase9 §5.5.
238    pub async fn get_node_by_external_id(
239        &self,
240        external_id: impl AsRef<str>,
241    ) -> Result<GetNodeResponse> {
242        let mut url = self.get_base_url().join("/data/nodes/by-external-id")?;
243        url.query_pairs_mut()
244            .append_pair("external_id", external_id.as_ref());
245        let mut request_builder = self.get_client().get(url);
246        request_builder = self.add_auth_headers(request_builder)?;
247        let response = self.execute_with_retry(request_builder).await?;
248        let status = response.status();
249        if status.is_success() {
250            Ok(response.json().await?)
251        } else {
252            let error_text = response
253                .text()
254                .await
255                .unwrap_or_else(|_| "Unknown error".to_string());
256            Err(NexusError::Api {
257                message: error_text,
258                status: status.as_u16(),
259            })
260        }
261    }
262
263    /// Get a node by ID
264    ///
265    /// # Arguments
266    ///
267    /// * `node_id` - ID of the node to retrieve
268    ///
269    /// # Example
270    ///
271    /// ```no_run
272    /// # use nexus_sdk::NexusClient;
273    /// # #[tokio::main]
274    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
275    /// # let client = NexusClient::new("http://localhost:15474")?;
276    /// let response = client.get_node(0).await?; // Replace 0 with an actual node ID
277    /// if let Some(node) = response.node {
278    ///     tracing::info!("Retrieved node: {:?}", node);
279    /// }
280    /// # Ok(())
281    /// # }
282    /// ```
283    pub async fn get_node(&self, node_id: u64) -> Result<GetNodeResponse> {
284        let url = self
285            .get_base_url()
286            .join(&format!("/data/nodes?id={}", node_id))?;
287        let mut request_builder = self.get_client().get(url);
288
289        request_builder = self.add_auth_headers(request_builder)?;
290
291        let response = self.execute_with_retry(request_builder).await?;
292        let status = response.status();
293
294        if status.is_success() {
295            let result: GetNodeResponse = response.json().await?;
296            Ok(result)
297        } else {
298            let error_text = response
299                .text()
300                .await
301                .unwrap_or_else(|_| "Unknown error".to_string());
302            Err(NexusError::Api {
303                message: error_text,
304                status: status.as_u16(),
305            })
306        }
307    }
308
309    /// Update an existing node
310    ///
311    /// # Arguments
312    ///
313    /// * `node_id` - ID of the node to update
314    /// * `properties` - New properties for the node
315    ///
316    /// # Example
317    ///
318    /// ```no_run
319    /// # use nexus_sdk::{NexusClient, Value};
320    /// # use std::collections::HashMap;
321    /// # #[tokio::main]
322    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
323    /// # let client = NexusClient::new("http://localhost:15474")?;
324    /// let mut properties = HashMap::new();
325    /// properties.insert("name".to_string(), Value::String("Bob".to_string()));
326    /// let response = client.update_node(0, properties).await?; // Replace 0 with an actual node ID
327    /// tracing::info!("Update node result: {}", response.message);
328    /// # Ok(())
329    /// # }
330    /// ```
331    pub async fn update_node(
332        &self,
333        node_id: u64,
334        properties: HashMap<String, Value>,
335    ) -> Result<UpdateNodeResponse> {
336        let request = UpdateNodeRequest {
337            node_id,
338            properties,
339        };
340
341        let url = self.get_base_url().join("/data/nodes")?;
342        let mut request_builder = self.get_client().put(url).json(&request);
343
344        request_builder = self.add_auth_headers(request_builder)?;
345
346        let response = self.execute_with_retry(request_builder).await?;
347        let status = response.status();
348
349        if status.is_success() {
350            let result: UpdateNodeResponse = response.json().await?;
351            Ok(result)
352        } else {
353            let error_text = response
354                .text()
355                .await
356                .unwrap_or_else(|_| "Unknown error".to_string());
357            Err(NexusError::Api {
358                message: error_text,
359                status: status.as_u16(),
360            })
361        }
362    }
363
364    /// Delete a node by ID
365    ///
366    /// # Arguments
367    ///
368    /// * `node_id` - ID of the node to delete
369    ///
370    /// # Example
371    ///
372    /// ```no_run
373    /// # use nexus_sdk::NexusClient;
374    /// # #[tokio::main]
375    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
376    /// # let client = NexusClient::new("http://localhost:15474")?;
377    /// let response = client.delete_node(0).await?; // Replace 0 with an actual node ID
378    /// tracing::info!("Delete result: {}", response.message);
379    /// # Ok(())
380    /// # }
381    /// ```
382    pub async fn delete_node(&self, node_id: u64) -> Result<DeleteNodeResponse> {
383        let url = self
384            .get_base_url()
385            .join(&format!("/data/nodes?id={}", node_id))?;
386        let mut request_builder = self.get_client().delete(url);
387
388        request_builder = self.add_auth_headers(request_builder)?;
389
390        let response = self.execute_with_retry(request_builder).await?;
391        let status = response.status();
392
393        if status.is_success() {
394            let result: DeleteNodeResponse = response.json().await?;
395            Ok(result)
396        } else {
397            let error_text = response
398                .text()
399                .await
400                .unwrap_or_else(|_| "Unknown error".to_string());
401            Err(NexusError::Api {
402                message: error_text,
403                status: status.as_u16(),
404            })
405        }
406    }
407
408    /// Create a new relationship
409    ///
410    /// # Arguments
411    ///
412    /// * `source_id` - ID of the source node
413    /// * `target_id` - ID of the target node
414    /// * `rel_type` - Type of the relationship
415    /// * `properties` - Optional relationship properties
416    ///
417    /// # Example
418    ///
419    /// ```no_run
420    /// # use nexus_sdk::{NexusClient, Value};
421    /// # use std::collections::HashMap;
422    /// # #[tokio::main]
423    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
424    /// # let client = NexusClient::new("http://localhost:15474")?;
425    /// let mut properties = HashMap::new();
426    /// properties.insert("weight".to_string(), Value::Float(1.5));
427    /// let response = client.create_relationship(1, 2, "KNOWS".to_string(), properties).await?;
428    /// tracing::info!("Created relationship with ID: {}", response.rel_id);
429    /// # Ok(())
430    /// # }
431    /// ```
432    pub async fn create_relationship(
433        &self,
434        source_id: u64,
435        target_id: u64,
436        rel_type: String,
437        properties: HashMap<String, Value>,
438    ) -> Result<CreateRelResponse> {
439        let request = CreateRelRequest {
440            source_id,
441            target_id,
442            rel_type,
443            properties,
444        };
445
446        let url = self.get_base_url().join("/data/relationships")?;
447        let mut request_builder = self.get_client().post(url).json(&request);
448
449        request_builder = self.add_auth_headers(request_builder)?;
450
451        let response = self.execute_with_retry(request_builder).await?;
452        let status = response.status();
453
454        if status.is_success() {
455            let result: CreateRelResponse = response.json().await?;
456            Ok(result)
457        } else {
458            let error_text = response
459                .text()
460                .await
461                .unwrap_or_else(|_| "Unknown error".to_string());
462            Err(NexusError::Api {
463                message: error_text,
464                status: status.as_u16(),
465            })
466        }
467    }
468
469    /// Update an existing relationship using Cypher
470    ///
471    /// # Arguments
472    ///
473    /// * `rel_id` - ID of the relationship to update
474    /// * `properties` - New properties for the relationship
475    ///
476    /// # Example
477    ///
478    /// ```no_run
479    /// # use nexus_sdk::{NexusClient, Value};
480    /// # use std::collections::HashMap;
481    /// # #[tokio::main]
482    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
483    /// # let client = NexusClient::new("http://localhost:15474")?;
484    /// let mut properties = HashMap::new();
485    /// properties.insert("weight".to_string(), Value::Float(2.0));
486    /// let response = client.update_relationship(1, properties).await?;
487    /// tracing::info!("Update relationship result: {}", response.message);
488    /// # Ok(())
489    /// # }
490    /// ```
491    pub async fn update_relationship(
492        &self,
493        rel_id: u64,
494        properties: HashMap<String, Value>,
495    ) -> Result<UpdateRelResponse> {
496        // Use Cypher SET to update relationship properties
497        let mut props_str = Vec::new();
498        let mut params = HashMap::new();
499
500        for (key, value) in properties {
501            let param_name = format!("prop_{}", key.replace('-', "_"));
502            props_str.push(format!("r.{} = ${}", key, param_name));
503            params.insert(param_name, value);
504        }
505
506        let query = format!(
507            "MATCH ()-[r]->() WHERE id(r) = $rel_id SET {} RETURN r",
508            props_str.join(", ")
509        );
510        params.insert("rel_id".to_string(), Value::Int(rel_id as i64));
511
512        let _result = self.execute_cypher(&query, Some(params)).await?;
513
514        Ok(UpdateRelResponse {
515            message: "Relationship updated successfully".to_string(),
516            error: None,
517        })
518    }
519
520    /// Delete a relationship using Cypher
521    ///
522    /// # Arguments
523    ///
524    /// * `rel_id` - ID of the relationship to delete
525    ///
526    /// # Example
527    ///
528    /// ```no_run
529    /// # use nexus_sdk::NexusClient;
530    /// # #[tokio::main]
531    /// # async fn main() -> Result<(), nexus_sdk::NexusError> {
532    /// # let client = NexusClient::new("http://localhost:15474")?;
533    /// let response = client.delete_relationship(1).await?;
534    /// tracing::info!("Delete relationship result: {}", response.message);
535    /// # Ok(())
536    /// # }
537    /// ```
538    pub async fn delete_relationship(&self, rel_id: u64) -> Result<DeleteRelResponse> {
539        let mut params = HashMap::new();
540        params.insert("rel_id".to_string(), Value::Int(rel_id as i64));
541
542        let query = "MATCH ()-[r]->() WHERE id(r) = $rel_id DELETE r RETURN count(r) as deleted";
543        let _result = self.execute_cypher(query, Some(params)).await?;
544
545        Ok(DeleteRelResponse {
546            message: "Relationship deleted successfully".to_string(),
547            error: None,
548        })
549    }
550}