ruvector_graph_node/
lib.rs

1//! Node.js bindings for RuVector Graph Database via NAPI-RS
2//!
3//! High-performance native graph database with Cypher-like query support,
4//! hypergraph capabilities, async/await support, and zero-copy buffer sharing.
5
6#![deny(clippy::all)]
7#![warn(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use ruvector_core::advanced::hypergraph::{
12    CausalMemory as CoreCausalMemory, Hyperedge as CoreHyperedge,
13    HypergraphIndex as CoreHypergraphIndex, HypergraphStats as CoreHypergraphStats,
14    TemporalGranularity as CoreTemporalGranularity, TemporalHyperedge as CoreTemporalHyperedge,
15};
16use ruvector_core::DistanceMetric;
17use std::collections::HashMap;
18use std::sync::{Arc, RwLock};
19
20mod streaming;
21mod transactions;
22mod types;
23
24pub use streaming::*;
25pub use transactions::*;
26pub use types::*;
27
28/// Graph database for complex relationship queries
29#[napi]
30pub struct GraphDatabase {
31    hypergraph: Arc<RwLock<CoreHypergraphIndex>>,
32    causal_memory: Arc<RwLock<CoreCausalMemory>>,
33    transaction_manager: Arc<RwLock<transactions::TransactionManager>>,
34}
35
36#[napi]
37impl GraphDatabase {
38    /// Create a new graph database
39    ///
40    /// # Example
41    /// ```javascript
42    /// const db = new GraphDatabase({
43    ///   distanceMetric: 'Cosine',
44    ///   dimensions: 384
45    /// });
46    /// ```
47    #[napi(constructor)]
48    pub fn new(options: Option<JsGraphOptions>) -> Result<Self> {
49        let opts = options.unwrap_or_default();
50        let metric = opts.distance_metric.unwrap_or(JsDistanceMetric::Cosine);
51        let core_metric: DistanceMetric = metric.into();
52
53        Ok(Self {
54            hypergraph: Arc::new(RwLock::new(CoreHypergraphIndex::new(core_metric))),
55            causal_memory: Arc::new(RwLock::new(CoreCausalMemory::new(core_metric))),
56            transaction_manager: Arc::new(RwLock::new(transactions::TransactionManager::new())),
57        })
58    }
59
60    /// Create a node in the graph
61    ///
62    /// # Example
63    /// ```javascript
64    /// const nodeId = await db.createNode({
65    ///   id: 'node1',
66    ///   embedding: new Float32Array([1, 2, 3]),
67    ///   properties: { name: 'Alice', age: 30 }
68    /// });
69    /// ```
70    #[napi]
71    pub async fn create_node(&self, node: JsNode) -> Result<String> {
72        let hypergraph = self.hypergraph.clone();
73        let id = node.id.clone();
74        let embedding = node.embedding.to_vec();
75
76        tokio::task::spawn_blocking(move || {
77            let mut hg = hypergraph.write().expect("RwLock poisoned");
78            hg.add_entity(id.clone(), embedding);
79            Ok::<String, Error>(id)
80        })
81        .await
82        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
83    }
84
85    /// Create an edge between two nodes
86    ///
87    /// # Example
88    /// ```javascript
89    /// const edgeId = await db.createEdge({
90    ///   from: 'node1',
91    ///   to: 'node2',
92    ///   description: 'knows',
93    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
94    ///   confidence: 0.95
95    /// });
96    /// ```
97    #[napi]
98    pub async fn create_edge(&self, edge: JsEdge) -> Result<String> {
99        let hypergraph = self.hypergraph.clone();
100        let nodes = vec![edge.from.clone(), edge.to.clone()];
101        let description = edge.description.clone();
102        let embedding = edge.embedding.to_vec();
103        let confidence = edge.confidence.unwrap_or(1.0) as f32;
104
105        tokio::task::spawn_blocking(move || {
106            let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
107            let edge_id = core_edge.id.clone();
108            let mut hg = hypergraph.write().expect("RwLock poisoned");
109            hg.add_hyperedge(core_edge)
110                .map_err(|e| Error::from_reason(format!("Failed to create edge: {}", e)))?;
111            Ok(edge_id)
112        })
113        .await
114        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
115    }
116
117    /// Create a hyperedge connecting multiple nodes
118    ///
119    /// # Example
120    /// ```javascript
121    /// const hyperedgeId = await db.createHyperedge({
122    ///   nodes: ['node1', 'node2', 'node3'],
123    ///   description: 'collaborated_on_project',
124    ///   embedding: new Float32Array([0.3, 0.6, 0.9]),
125    ///   confidence: 0.85,
126    ///   metadata: { project: 'AI Research' }
127    /// });
128    /// ```
129    #[napi]
130    pub async fn create_hyperedge(&self, hyperedge: JsHyperedge) -> Result<String> {
131        let hypergraph = self.hypergraph.clone();
132        let nodes = hyperedge.nodes.clone();
133        let description = hyperedge.description.clone();
134        let embedding = hyperedge.embedding.to_vec();
135        let confidence = hyperedge.confidence.unwrap_or(1.0) as f32;
136
137        tokio::task::spawn_blocking(move || {
138            let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
139            let edge_id = core_edge.id.clone();
140            let mut hg = hypergraph.write().expect("RwLock poisoned");
141            hg.add_hyperedge(core_edge)
142                .map_err(|e| Error::from_reason(format!("Failed to create hyperedge: {}", e)))?;
143            Ok(edge_id)
144        })
145        .await
146        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
147    }
148
149    /// Query the graph using Cypher-like syntax (simplified)
150    ///
151    /// # Example
152    /// ```javascript
153    /// const results = await db.query('MATCH (n) RETURN n LIMIT 10');
154    /// ```
155    #[napi]
156    pub async fn query(&self, cypher: String) -> Result<JsQueryResult> {
157        // Parse and execute Cypher query
158        let hypergraph = self.hypergraph.clone();
159
160        tokio::task::spawn_blocking(move || {
161            let hg = hypergraph.read().expect("RwLock poisoned");
162            let stats = hg.stats();
163
164            // Simplified query result for now
165            Ok::<JsQueryResult, Error>(JsQueryResult {
166                nodes: vec![],
167                edges: vec![],
168                stats: Some(JsGraphStats {
169                    total_nodes: stats.total_entities as u32,
170                    total_edges: stats.total_hyperedges as u32,
171                    avg_degree: stats.avg_entity_degree as f64,
172                }),
173            })
174        })
175        .await
176        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
177    }
178
179    /// Query the graph synchronously
180    ///
181    /// # Example
182    /// ```javascript
183    /// const results = db.querySync('MATCH (n) RETURN n LIMIT 10');
184    /// ```
185    #[napi]
186    pub fn query_sync(&self, cypher: String) -> Result<JsQueryResult> {
187        let hg = self.hypergraph.read().expect("RwLock poisoned");
188        let stats = hg.stats();
189
190        // Simplified query result for now
191        Ok(JsQueryResult {
192            nodes: vec![],
193            edges: vec![],
194            stats: Some(JsGraphStats {
195                total_nodes: stats.total_entities as u32,
196                total_edges: stats.total_hyperedges as u32,
197                avg_degree: stats.avg_entity_degree as f64,
198            }),
199        })
200    }
201
202    /// Search for similar hyperedges
203    ///
204    /// # Example
205    /// ```javascript
206    /// const results = await db.searchHyperedges({
207    ///   embedding: new Float32Array([0.5, 0.5, 0.5]),
208    ///   k: 10
209    /// });
210    /// ```
211    #[napi]
212    pub async fn search_hyperedges(
213        &self,
214        query: JsHyperedgeQuery,
215    ) -> Result<Vec<JsHyperedgeResult>> {
216        let hypergraph = self.hypergraph.clone();
217        let embedding = query.embedding.to_vec();
218        let k = query.k as usize;
219
220        tokio::task::spawn_blocking(move || {
221            let hg = hypergraph.read().expect("RwLock poisoned");
222            let results = hg.search_hyperedges(&embedding, k);
223
224            Ok::<Vec<JsHyperedgeResult>, Error>(
225                results
226                    .into_iter()
227                    .map(|(id, score)| JsHyperedgeResult {
228                        id,
229                        score: f64::from(score),
230                    })
231                    .collect(),
232            )
233        })
234        .await
235        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
236    }
237
238    /// Get k-hop neighbors from a starting node
239    ///
240    /// # Example
241    /// ```javascript
242    /// const neighbors = await db.kHopNeighbors('node1', 2);
243    /// ```
244    #[napi]
245    pub async fn k_hop_neighbors(&self, start_node: String, k: u32) -> Result<Vec<String>> {
246        let hypergraph = self.hypergraph.clone();
247        let hops = k as usize;
248
249        tokio::task::spawn_blocking(move || {
250            let hg = hypergraph.read().expect("RwLock poisoned");
251            let neighbors = hg.k_hop_neighbors(start_node, hops);
252            Ok::<Vec<String>, Error>(neighbors.into_iter().collect())
253        })
254        .await
255        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
256    }
257
258    /// Begin a new transaction
259    ///
260    /// # Example
261    /// ```javascript
262    /// const txId = await db.begin();
263    /// ```
264    #[napi]
265    pub async fn begin(&self) -> Result<String> {
266        let tm = self.transaction_manager.clone();
267
268        tokio::task::spawn_blocking(move || {
269            let mut manager = tm.write().expect("RwLock poisoned");
270            Ok::<String, Error>(manager.begin())
271        })
272        .await
273        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
274    }
275
276    /// Commit a transaction
277    ///
278    /// # Example
279    /// ```javascript
280    /// await db.commit(txId);
281    /// ```
282    #[napi]
283    pub async fn commit(&self, tx_id: String) -> Result<()> {
284        let tm = self.transaction_manager.clone();
285
286        tokio::task::spawn_blocking(move || {
287            let mut manager = tm.write().expect("RwLock poisoned");
288            manager
289                .commit(&tx_id)
290                .map_err(|e| Error::from_reason(format!("Failed to commit: {}", e)))
291        })
292        .await
293        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
294    }
295
296    /// Rollback a transaction
297    ///
298    /// # Example
299    /// ```javascript
300    /// await db.rollback(txId);
301    /// ```
302    #[napi]
303    pub async fn rollback(&self, tx_id: String) -> Result<()> {
304        let tm = self.transaction_manager.clone();
305
306        tokio::task::spawn_blocking(move || {
307            let mut manager = tm.write().expect("RwLock poisoned");
308            manager
309                .rollback(&tx_id)
310                .map_err(|e| Error::from_reason(format!("Failed to rollback: {}", e)))
311        })
312        .await
313        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
314    }
315
316    /// Batch insert nodes and edges
317    ///
318    /// # Example
319    /// ```javascript
320    /// await db.batchInsert({
321    ///   nodes: [{ id: 'n1', embedding: new Float32Array([1, 2]) }],
322    ///   edges: [{ from: 'n1', to: 'n2', description: 'knows' }]
323    /// });
324    /// ```
325    #[napi]
326    pub async fn batch_insert(&self, batch: JsBatchInsert) -> Result<JsBatchResult> {
327        let hypergraph = self.hypergraph.clone();
328        let nodes = batch.nodes;
329        let edges = batch.edges;
330
331        tokio::task::spawn_blocking(move || {
332            let mut hg = hypergraph.write().expect("RwLock poisoned");
333            let mut node_ids = Vec::new();
334            let mut edge_ids = Vec::new();
335
336            // Insert nodes
337            for node in nodes {
338                hg.add_entity(node.id.clone(), node.embedding.to_vec());
339                node_ids.push(node.id);
340            }
341
342            // Insert edges
343            for edge in edges {
344                let nodes = vec![edge.from.clone(), edge.to.clone()];
345                let embedding = edge.embedding.to_vec();
346                let confidence = edge.confidence.unwrap_or(1.0) as f32;
347                let core_edge = CoreHyperedge::new(nodes, edge.description, embedding, confidence);
348                let edge_id = core_edge.id.clone();
349                hg.add_hyperedge(core_edge)
350                    .map_err(|e| Error::from_reason(format!("Failed to insert edge: {}", e)))?;
351                edge_ids.push(edge_id);
352            }
353
354            Ok::<JsBatchResult, Error>(JsBatchResult { node_ids, edge_ids })
355        })
356        .await
357        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
358    }
359
360    /// Subscribe to graph changes (returns a change stream)
361    ///
362    /// # Example
363    /// ```javascript
364    /// const unsubscribe = db.subscribe((change) => {
365    ///   console.log('Graph changed:', change);
366    /// });
367    /// ```
368    #[napi]
369    pub fn subscribe(&self, callback: JsFunction) -> Result<()> {
370        // Placeholder for event emitter pattern
371        // In a real implementation, this would set up a change listener
372        Ok(())
373    }
374
375    /// Get graph statistics
376    ///
377    /// # Example
378    /// ```javascript
379    /// const stats = await db.stats();
380    /// console.log(`Nodes: ${stats.totalNodes}, Edges: ${stats.totalEdges}`);
381    /// ```
382    #[napi]
383    pub async fn stats(&self) -> Result<JsGraphStats> {
384        let hypergraph = self.hypergraph.clone();
385
386        tokio::task::spawn_blocking(move || {
387            let hg = hypergraph.read().expect("RwLock poisoned");
388            let stats = hg.stats();
389
390            Ok::<JsGraphStats, Error>(JsGraphStats {
391                total_nodes: stats.total_entities as u32,
392                total_edges: stats.total_hyperedges as u32,
393                avg_degree: stats.avg_entity_degree as f64,
394            })
395        })
396        .await
397        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
398    }
399}
400
401/// Get the version of the library
402#[napi]
403pub fn version() -> String {
404    env!("CARGO_PKG_VERSION").to_string()
405}
406
407/// Test function to verify bindings
408#[napi]
409pub fn hello() -> String {
410    "Hello from RuVector Graph Node.js bindings!".to_string()
411}