1#![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,
14};
15use ruvector_core::DistanceMetric;
16use ruvector_graph::cypher::{parse_cypher, Statement};
17use ruvector_graph::node::NodeBuilder;
18use ruvector_graph::storage::GraphStorage;
19use ruvector_graph::GraphDB;
20use std::sync::{Arc, RwLock};
21
22mod streaming;
23mod transactions;
24mod types;
25
26pub use streaming::*;
27pub use transactions::*;
28pub use types::*;
29
30#[napi]
32pub struct GraphDatabase {
33 hypergraph: Arc<RwLock<CoreHypergraphIndex>>,
34 causal_memory: Arc<RwLock<CoreCausalMemory>>,
35 transaction_manager: Arc<RwLock<transactions::TransactionManager>>,
36 graph_db: Arc<RwLock<GraphDB>>,
38 storage: Option<Arc<RwLock<GraphStorage>>>,
40 storage_path: Option<String>,
42}
43
44#[napi]
45impl GraphDatabase {
46 #[napi(constructor)]
56 pub fn new(options: Option<JsGraphOptions>) -> Result<Self> {
57 let opts = options.unwrap_or_default();
58 let metric = opts.distance_metric.unwrap_or(JsDistanceMetric::Cosine);
59 let core_metric: DistanceMetric = metric.into();
60
61 let (storage, storage_path) = if let Some(ref path) = opts.storage_path {
63 let gs = GraphStorage::new(path)
64 .map_err(|e| Error::from_reason(format!("Failed to open storage: {}", e)))?;
65 (Some(Arc::new(RwLock::new(gs))), Some(path.clone()))
66 } else {
67 (None, None)
68 };
69
70 Ok(Self {
71 hypergraph: Arc::new(RwLock::new(CoreHypergraphIndex::new(core_metric))),
72 causal_memory: Arc::new(RwLock::new(CoreCausalMemory::new(core_metric))),
73 transaction_manager: Arc::new(RwLock::new(transactions::TransactionManager::new())),
74 graph_db: Arc::new(RwLock::new(GraphDB::new())),
75 storage,
76 storage_path,
77 })
78 }
79
80 #[napi(factory)]
87 pub fn open(path: String) -> Result<Self> {
88 let storage = GraphStorage::new(&path)
89 .map_err(|e| Error::from_reason(format!("Failed to open storage: {}", e)))?;
90
91 let metric = DistanceMetric::Cosine;
92
93 Ok(Self {
94 hypergraph: Arc::new(RwLock::new(CoreHypergraphIndex::new(metric))),
95 causal_memory: Arc::new(RwLock::new(CoreCausalMemory::new(metric))),
96 transaction_manager: Arc::new(RwLock::new(transactions::TransactionManager::new())),
97 graph_db: Arc::new(RwLock::new(GraphDB::new())),
98 storage: Some(Arc::new(RwLock::new(storage))),
99 storage_path: Some(path),
100 })
101 }
102
103 #[napi]
112 pub fn is_persistent(&self) -> bool {
113 self.storage.is_some()
114 }
115
116 #[napi]
118 pub fn get_storage_path(&self) -> Option<String> {
119 self.storage_path.clone()
120 }
121
122 #[napi]
133 pub async fn create_node(&self, node: JsNode) -> Result<String> {
134 let hypergraph = self.hypergraph.clone();
135 let graph_db = self.graph_db.clone();
136 let storage = self.storage.clone();
137 let id = node.id.clone();
138 let embedding = node.embedding.to_vec();
139 let properties = node.properties.clone();
140 let labels = node.labels.clone();
141
142 tokio::task::spawn_blocking(move || {
143 let mut hg = hypergraph.write().expect("RwLock poisoned");
145 hg.add_entity(id.clone(), embedding);
146
147 let mut gdb = graph_db.write().expect("RwLock poisoned");
149 let mut builder = NodeBuilder::new().id(&id);
150
151 if let Some(node_labels) = labels {
153 for label in node_labels {
154 builder = builder.label(&label);
155 }
156 }
157
158 if let Some(props) = properties {
160 for (key, value) in props {
161 builder = builder.property(&key, value);
162 }
163 }
164
165 let graph_node = builder.build();
166
167 if let Some(ref storage_arc) = storage {
169 let storage_guard = storage_arc.write().expect("Storage RwLock poisoned");
170 storage_guard
171 .insert_node(&graph_node)
172 .map_err(|e| Error::from_reason(format!("Failed to persist node: {}", e)))?;
173 }
174
175 gdb.create_node(graph_node)
176 .map_err(|e| Error::from_reason(format!("Failed to create node: {}", e)))?;
177
178 Ok::<String, Error>(id)
179 })
180 .await
181 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
182 }
183
184 #[napi]
197 pub async fn create_edge(&self, edge: JsEdge) -> Result<String> {
198 let hypergraph = self.hypergraph.clone();
199 let nodes = vec![edge.from.clone(), edge.to.clone()];
200 let description = edge.description.clone();
201 let embedding = edge.embedding.to_vec();
202 let confidence = edge.confidence.unwrap_or(1.0) as f32;
203
204 tokio::task::spawn_blocking(move || {
205 let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
206 let edge_id = core_edge.id.clone();
207 let mut hg = hypergraph.write().expect("RwLock poisoned");
208 hg.add_hyperedge(core_edge)
209 .map_err(|e| Error::from_reason(format!("Failed to create edge: {}", e)))?;
210 Ok(edge_id)
211 })
212 .await
213 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
214 }
215
216 #[napi]
229 pub async fn create_hyperedge(&self, hyperedge: JsHyperedge) -> Result<String> {
230 let hypergraph = self.hypergraph.clone();
231 let nodes = hyperedge.nodes.clone();
232 let description = hyperedge.description.clone();
233 let embedding = hyperedge.embedding.to_vec();
234 let confidence = hyperedge.confidence.unwrap_or(1.0) as f32;
235
236 tokio::task::spawn_blocking(move || {
237 let core_edge = CoreHyperedge::new(nodes, description, embedding, confidence);
238 let edge_id = core_edge.id.clone();
239 let mut hg = hypergraph.write().expect("RwLock poisoned");
240 hg.add_hyperedge(core_edge)
241 .map_err(|e| Error::from_reason(format!("Failed to create hyperedge: {}", e)))?;
242 Ok(edge_id)
243 })
244 .await
245 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
246 }
247
248 #[napi]
255 pub async fn query(&self, cypher: String) -> Result<JsQueryResult> {
256 let graph_db = self.graph_db.clone();
257 let hypergraph = self.hypergraph.clone();
258
259 tokio::task::spawn_blocking(move || {
260 let parsed = parse_cypher(&cypher)
262 .map_err(|e| Error::from_reason(format!("Cypher parse error: {}", e)))?;
263
264 let gdb = graph_db.read().expect("RwLock poisoned");
265 let hg = hypergraph.read().expect("RwLock poisoned");
266
267 let mut result_nodes: Vec<JsNodeResult> = Vec::new();
268 let mut result_edges: Vec<JsEdgeResult> = Vec::new();
269
270 for statement in &parsed.statements {
272 match statement {
273 Statement::Match(match_clause) => {
274 for pattern in &match_clause.patterns {
276 if let ruvector_graph::cypher::ast::Pattern::Node(node_pattern) =
277 pattern
278 {
279 for label in &node_pattern.labels {
280 let nodes = gdb.get_nodes_by_label(label);
281 for node in nodes {
282 result_nodes.push(JsNodeResult {
283 id: node.id.clone(),
284 labels: node
285 .labels
286 .iter()
287 .map(|l| l.name.clone())
288 .collect(),
289 properties: node
290 .properties
291 .iter()
292 .map(|(k, v)| (k.clone(), format!("{:?}", v)))
293 .collect(),
294 });
295 }
296 }
297 if node_pattern.labels.is_empty() && node_pattern.variable.is_some()
299 {
300 }
302 }
303 }
304 }
305 Statement::Create(create_clause) => {
306 }
308 Statement::Return(_) => {
309 }
311 _ => {}
312 }
313 }
314
315 let stats = hg.stats();
316
317 Ok::<JsQueryResult, Error>(JsQueryResult {
318 nodes: result_nodes,
319 edges: result_edges,
320 stats: Some(JsGraphStats {
321 total_nodes: stats.total_entities as u32,
322 total_edges: stats.total_hyperedges as u32,
323 avg_degree: stats.avg_entity_degree as f64,
324 }),
325 })
326 })
327 .await
328 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
329 }
330
331 #[napi]
338 pub fn query_sync(&self, cypher: String) -> Result<JsQueryResult> {
339 let hg = self.hypergraph.read().expect("RwLock poisoned");
340 let stats = hg.stats();
341
342 Ok(JsQueryResult {
344 nodes: vec![],
345 edges: vec![],
346 stats: Some(JsGraphStats {
347 total_nodes: stats.total_entities as u32,
348 total_edges: stats.total_hyperedges as u32,
349 avg_degree: stats.avg_entity_degree as f64,
350 }),
351 })
352 }
353
354 #[napi]
364 pub async fn search_hyperedges(
365 &self,
366 query: JsHyperedgeQuery,
367 ) -> Result<Vec<JsHyperedgeResult>> {
368 let hypergraph = self.hypergraph.clone();
369 let embedding = query.embedding.to_vec();
370 let k = query.k as usize;
371
372 tokio::task::spawn_blocking(move || {
373 let hg = hypergraph.read().expect("RwLock poisoned");
374 let results = hg.search_hyperedges(&embedding, k);
375
376 Ok::<Vec<JsHyperedgeResult>, Error>(
377 results
378 .into_iter()
379 .map(|(id, score)| JsHyperedgeResult {
380 id,
381 score: f64::from(score),
382 })
383 .collect(),
384 )
385 })
386 .await
387 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
388 }
389
390 #[napi]
397 pub async fn k_hop_neighbors(&self, start_node: String, k: u32) -> Result<Vec<String>> {
398 let hypergraph = self.hypergraph.clone();
399 let hops = k as usize;
400
401 tokio::task::spawn_blocking(move || {
402 let hg = hypergraph.read().expect("RwLock poisoned");
403 let neighbors = hg.k_hop_neighbors(start_node, hops);
404 Ok::<Vec<String>, Error>(neighbors.into_iter().collect())
405 })
406 .await
407 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
408 }
409
410 #[napi]
417 pub async fn begin(&self) -> Result<String> {
418 let tm = self.transaction_manager.clone();
419
420 tokio::task::spawn_blocking(move || {
421 let mut manager = tm.write().expect("RwLock poisoned");
422 Ok::<String, Error>(manager.begin())
423 })
424 .await
425 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
426 }
427
428 #[napi]
435 pub async fn commit(&self, tx_id: String) -> Result<()> {
436 let tm = self.transaction_manager.clone();
437
438 tokio::task::spawn_blocking(move || {
439 let mut manager = tm.write().expect("RwLock poisoned");
440 manager
441 .commit(&tx_id)
442 .map_err(|e| Error::from_reason(format!("Failed to commit: {}", e)))
443 })
444 .await
445 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
446 }
447
448 #[napi]
455 pub async fn rollback(&self, tx_id: String) -> Result<()> {
456 let tm = self.transaction_manager.clone();
457
458 tokio::task::spawn_blocking(move || {
459 let mut manager = tm.write().expect("RwLock poisoned");
460 manager
461 .rollback(&tx_id)
462 .map_err(|e| Error::from_reason(format!("Failed to rollback: {}", e)))
463 })
464 .await
465 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
466 }
467
468 #[napi]
478 pub async fn batch_insert(&self, batch: JsBatchInsert) -> Result<JsBatchResult> {
479 let hypergraph = self.hypergraph.clone();
480 let nodes = batch.nodes;
481 let edges = batch.edges;
482
483 tokio::task::spawn_blocking(move || {
484 let mut hg = hypergraph.write().expect("RwLock poisoned");
485 let mut node_ids = Vec::new();
486 let mut edge_ids = Vec::new();
487
488 for node in nodes {
490 hg.add_entity(node.id.clone(), node.embedding.to_vec());
491 node_ids.push(node.id);
492 }
493
494 for edge in edges {
496 let nodes = vec![edge.from.clone(), edge.to.clone()];
497 let embedding = edge.embedding.to_vec();
498 let confidence = edge.confidence.unwrap_or(1.0) as f32;
499 let core_edge = CoreHyperedge::new(nodes, edge.description, embedding, confidence);
500 let edge_id = core_edge.id.clone();
501 hg.add_hyperedge(core_edge)
502 .map_err(|e| Error::from_reason(format!("Failed to insert edge: {}", e)))?;
503 edge_ids.push(edge_id);
504 }
505
506 Ok::<JsBatchResult, Error>(JsBatchResult { node_ids, edge_ids })
507 })
508 .await
509 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
510 }
511
512 #[napi]
521 pub fn subscribe(&self, callback: JsFunction) -> Result<()> {
522 Ok(())
525 }
526
527 #[napi]
535 pub async fn stats(&self) -> Result<JsGraphStats> {
536 let hypergraph = self.hypergraph.clone();
537
538 tokio::task::spawn_blocking(move || {
539 let hg = hypergraph.read().expect("RwLock poisoned");
540 let stats = hg.stats();
541
542 Ok::<JsGraphStats, Error>(JsGraphStats {
543 total_nodes: stats.total_entities as u32,
544 total_edges: stats.total_hyperedges as u32,
545 avg_degree: stats.avg_entity_degree as f64,
546 })
547 })
548 .await
549 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
550 }
551}
552
553#[napi]
555pub fn version() -> String {
556 env!("CARGO_PKG_VERSION").to_string()
557}
558
559#[napi]
561pub fn hello() -> String {
562 "Hello from RuVector Graph Node.js bindings!".to_string()
563}