1use js_sys::{Array, Object, Promise, Reflect};
15use parking_lot::Mutex;
16use ruvector_core::advanced::hypergraph::{
17 Hyperedge as CoreHyperedge, HypergraphIndex, TemporalGranularity, TemporalHyperedge,
18};
19use ruvector_core::types::DistanceMetric;
20use serde_wasm_bindgen::{from_value, to_value};
21use std::collections::HashMap;
22use std::sync::Arc;
23use uuid::Uuid;
24use wasm_bindgen::prelude::*;
25use web_sys::console;
26
27pub mod async_ops;
28pub mod types;
29
30use types::{
31 js_object_to_hashmap, Edge, EdgeId, GraphError, Hyperedge, HyperedgeId, JsEdge, JsHyperedge,
32 JsNode, Node, NodeId, QueryResult,
33};
34
35#[wasm_bindgen(start)]
37pub fn init() {
38 console_error_panic_hook::set_once();
39 tracing_wasm::set_as_global_default();
40}
41
42#[wasm_bindgen]
44pub struct GraphDB {
45 nodes: Arc<Mutex<HashMap<NodeId, Node>>>,
46 edges: Arc<Mutex<HashMap<EdgeId, Edge>>>,
47 hypergraph: Arc<Mutex<HypergraphIndex>>,
48 hyperedges: Arc<Mutex<HashMap<HyperedgeId, Hyperedge>>>,
49 labels_index: Arc<Mutex<HashMap<String, Vec<NodeId>>>>,
51 edge_types_index: Arc<Mutex<HashMap<String, Vec<EdgeId>>>>,
52 node_edges_out: Arc<Mutex<HashMap<NodeId, Vec<EdgeId>>>>,
53 node_edges_in: Arc<Mutex<HashMap<NodeId, Vec<EdgeId>>>>,
54 distance_metric: DistanceMetric,
55}
56
57#[wasm_bindgen]
58impl GraphDB {
59 #[wasm_bindgen(constructor)]
64 pub fn new(metric: Option<String>) -> Result<GraphDB, JsValue> {
65 let distance_metric = match metric.as_deref() {
66 Some("euclidean") => DistanceMetric::Euclidean,
67 Some("cosine") => DistanceMetric::Cosine,
68 Some("dotproduct") => DistanceMetric::DotProduct,
69 Some("manhattan") => DistanceMetric::Manhattan,
70 None => DistanceMetric::Cosine,
71 Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
72 };
73
74 Ok(GraphDB {
75 nodes: Arc::new(Mutex::new(HashMap::new())),
76 edges: Arc::new(Mutex::new(HashMap::new())),
77 hypergraph: Arc::new(Mutex::new(HypergraphIndex::new(distance_metric))),
78 hyperedges: Arc::new(Mutex::new(HashMap::new())),
79 labels_index: Arc::new(Mutex::new(HashMap::new())),
80 edge_types_index: Arc::new(Mutex::new(HashMap::new())),
81 node_edges_out: Arc::new(Mutex::new(HashMap::new())),
82 node_edges_in: Arc::new(Mutex::new(HashMap::new())),
83 distance_metric,
84 })
85 }
86
87 #[wasm_bindgen]
95 pub async fn query(&self, cypher: String) -> Result<QueryResult, JsValue> {
96 console::log_1(&format!("Executing Cypher: {}", cypher).into());
97
98 let result = self
101 .execute_cypher(&cypher)
102 .map_err(|e| JsValue::from(GraphError::from(e)))?;
103
104 Ok(result)
105 }
106
107 #[wasm_bindgen(js_name = createNode)]
116 pub fn create_node(&self, labels: Vec<String>, properties: JsValue) -> Result<String, JsValue> {
117 let id = Uuid::new_v4().to_string();
118 let props = js_object_to_hashmap(properties).map_err(|e| JsValue::from_str(&e))?;
119
120 let embedding = props
122 .get("embedding")
123 .and_then(|v| serde_json::from_value::<Vec<f32>>(v.clone()).ok());
124
125 let node = Node {
126 id: id.clone(),
127 labels: labels.clone(),
128 properties: props,
129 embedding: embedding.clone(),
130 };
131
132 self.nodes.lock().insert(id.clone(), node);
134
135 let mut labels_index = self.labels_index.lock();
137 for label in &labels {
138 labels_index
139 .entry(label.clone())
140 .or_insert_with(Vec::new)
141 .push(id.clone());
142 }
143
144 if let Some(emb) = embedding {
146 self.hypergraph.lock().add_entity(id.clone(), emb);
147 }
148
149 self.node_edges_out.lock().insert(id.clone(), Vec::new());
151 self.node_edges_in.lock().insert(id.clone(), Vec::new());
152
153 Ok(id)
154 }
155
156 #[wasm_bindgen(js_name = createEdge)]
167 pub fn create_edge(
168 &self,
169 from: String,
170 to: String,
171 edge_type: String,
172 properties: JsValue,
173 ) -> Result<String, JsValue> {
174 let nodes = self.nodes.lock();
176 if !nodes.contains_key(&from) {
177 return Err(JsValue::from_str(&format!("Node {} not found", from)));
178 }
179 if !nodes.contains_key(&to) {
180 return Err(JsValue::from_str(&format!("Node {} not found", to)));
181 }
182 drop(nodes);
183
184 let id = Uuid::new_v4().to_string();
185 let props = js_object_to_hashmap(properties).map_err(|e| JsValue::from_str(&e))?;
186
187 let edge = Edge {
188 id: id.clone(),
189 from: from.clone(),
190 to: to.clone(),
191 edge_type: edge_type.clone(),
192 properties: props,
193 };
194
195 self.edges.lock().insert(id.clone(), edge);
197
198 self.edge_types_index
200 .lock()
201 .entry(edge_type)
202 .or_insert_with(Vec::new)
203 .push(id.clone());
204
205 self.node_edges_out
206 .lock()
207 .entry(from)
208 .or_insert_with(Vec::new)
209 .push(id.clone());
210
211 self.node_edges_in
212 .lock()
213 .entry(to)
214 .or_insert_with(Vec::new)
215 .push(id.clone());
216
217 Ok(id)
218 }
219
220 #[wasm_bindgen(js_name = createHyperedge)]
231 pub fn create_hyperedge(
232 &self,
233 nodes: Vec<String>,
234 description: String,
235 embedding: Option<Vec<f32>>,
236 confidence: Option<f32>,
237 ) -> Result<String, JsValue> {
238 let nodes_map = self.nodes.lock();
240 for node_id in &nodes {
241 if !nodes_map.contains_key(node_id) {
242 return Err(JsValue::from_str(&format!("Node {} not found", node_id)));
243 }
244 }
245 drop(nodes_map);
246
247 let id = Uuid::new_v4().to_string();
248
249 let emb = if let Some(e) = embedding {
251 e
252 } else {
253 self.generate_hyperedge_embedding(&nodes)?
254 };
255
256 let conf = confidence.unwrap_or(1.0).clamp(0.0, 1.0);
257
258 let hyperedge = Hyperedge {
259 id: id.clone(),
260 nodes: nodes.clone(),
261 description: description.clone(),
262 embedding: emb.clone(),
263 confidence: conf,
264 properties: HashMap::new(),
265 };
266
267 let core_hyperedge = CoreHyperedge {
269 id: id.clone(),
270 nodes: nodes.clone(),
271 description,
272 embedding: emb,
273 confidence: conf,
274 metadata: HashMap::new(),
275 };
276
277 self.hypergraph
279 .lock()
280 .add_hyperedge(core_hyperedge)
281 .map_err(|e| JsValue::from_str(&format!("Failed to add hyperedge: {}", e)))?;
282
283 self.hyperedges.lock().insert(id.clone(), hyperedge);
285
286 Ok(id)
287 }
288
289 #[wasm_bindgen(js_name = getNode)]
297 pub fn get_node(&self, id: String) -> Option<JsNode> {
298 self.nodes.lock().get(&id).map(|n| n.to_js())
299 }
300
301 #[wasm_bindgen(js_name = getEdge)]
303 pub fn get_edge(&self, id: String) -> Option<JsEdge> {
304 self.edges.lock().get(&id).map(|e| e.to_js())
305 }
306
307 #[wasm_bindgen(js_name = getHyperedge)]
309 pub fn get_hyperedge(&self, id: String) -> Option<JsHyperedge> {
310 self.hyperedges.lock().get(&id).map(|h| h.to_js())
311 }
312
313 #[wasm_bindgen(js_name = deleteNode)]
321 pub fn delete_node(&self, id: String) -> bool {
322 let removed = self.nodes.lock().remove(&id).is_some();
324
325 if removed {
326 let mut labels_index = self.labels_index.lock();
328 for (_, node_ids) in labels_index.iter_mut() {
329 node_ids.retain(|nid| nid != &id);
330 }
331
332 if let Some(out_edges) = self.node_edges_out.lock().remove(&id) {
334 for edge_id in out_edges {
335 self.edges.lock().remove(&edge_id);
336 }
337 }
338 if let Some(in_edges) = self.node_edges_in.lock().remove(&id) {
339 for edge_id in in_edges {
340 self.edges.lock().remove(&edge_id);
341 }
342 }
343 }
344
345 removed
346 }
347
348 #[wasm_bindgen(js_name = deleteEdge)]
350 pub fn delete_edge(&self, id: String) -> bool {
351 if let Some(edge) = self.edges.lock().remove(&id) {
352 if let Some(edges) = self.node_edges_out.lock().get_mut(&edge.from) {
354 edges.retain(|eid| eid != &id);
355 }
356 if let Some(edges) = self.node_edges_in.lock().get_mut(&edge.to) {
357 edges.retain(|eid| eid != &id);
358 }
359 true
360 } else {
361 false
362 }
363 }
364
365 #[wasm_bindgen(js_name = importCypher)]
373 pub async fn import_cypher(&self, statements: Vec<String>) -> Result<usize, JsValue> {
374 let mut count = 0;
375 for statement in statements {
376 self.execute_cypher(&statement)
377 .map_err(|e| JsValue::from_str(&e))?;
378 count += 1;
379 }
380 Ok(count)
381 }
382
383 #[wasm_bindgen(js_name = exportCypher)]
388 pub fn export_cypher(&self) -> String {
389 let mut cypher = String::new();
390
391 for (id, node) in self.nodes.lock().iter() {
393 let labels = if node.labels.is_empty() {
394 String::new()
395 } else {
396 format!(":{}", node.labels.join(":"))
397 };
398
399 let props = if node.properties.is_empty() {
400 String::new()
401 } else {
402 format!(
403 " {{{}}}",
404 node.properties
405 .iter()
406 .map(|(k, v)| format!("{}: {}", k, v))
407 .collect::<Vec<_>>()
408 .join(", ")
409 )
410 };
411
412 cypher.push_str(&format!("CREATE (n{}{})\n", labels, props));
413 }
414
415 for (id, edge) in self.edges.lock().iter() {
417 let props = if edge.properties.is_empty() {
418 String::new()
419 } else {
420 format!(
421 " {{{}}}",
422 edge.properties
423 .iter()
424 .map(|(k, v)| format!("{}: {}", k, v))
425 .collect::<Vec<_>>()
426 .join(", ")
427 )
428 };
429
430 cypher.push_str(&format!(
431 "MATCH (a), (b) WHERE id(a) = '{}' AND id(b) = '{}' CREATE (a)-[:{}{}]->(b)\n",
432 edge.from, edge.to, edge.edge_type, props
433 ));
434 }
435
436 cypher
437 }
438
439 #[wasm_bindgen]
441 pub fn stats(&self) -> JsValue {
442 let node_count = self.nodes.lock().len();
443 let edge_count = self.edges.lock().len();
444 let hyperedge_count = self.hyperedges.lock().len();
445 let hypergraph_stats = self.hypergraph.lock().stats();
446
447 let obj = Object::new();
448 Reflect::set(&obj, &"nodeCount".into(), &JsValue::from(node_count)).unwrap();
449 Reflect::set(&obj, &"edgeCount".into(), &JsValue::from(edge_count)).unwrap();
450 Reflect::set(
451 &obj,
452 &"hyperedgeCount".into(),
453 &JsValue::from(hyperedge_count),
454 )
455 .unwrap();
456 Reflect::set(
457 &obj,
458 &"hypergraphEntities".into(),
459 &JsValue::from(hypergraph_stats.total_entities),
460 )
461 .unwrap();
462 Reflect::set(
463 &obj,
464 &"hypergraphEdges".into(),
465 &JsValue::from(hypergraph_stats.total_hyperedges),
466 )
467 .unwrap();
468 Reflect::set(
469 &obj,
470 &"avgEntityDegree".into(),
471 &JsValue::from(hypergraph_stats.avg_entity_degree),
472 )
473 .unwrap();
474
475 obj.into()
476 }
477}
478
479impl GraphDB {
481 fn execute_cypher(&self, cypher: &str) -> Result<QueryResult, String> {
482 let cypher = cypher.trim();
483
484 if cypher.to_uppercase().starts_with("MATCH") {
486 self.execute_match_query(cypher)
487 } else if cypher.to_uppercase().starts_with("CREATE") {
488 self.execute_create_query(cypher)
489 } else {
490 Err(format!("Unsupported Cypher statement: {}", cypher))
491 }
492 }
493
494 fn execute_match_query(&self, _cypher: &str) -> Result<QueryResult, String> {
495 Ok(QueryResult {
499 nodes: Vec::new(),
500 edges: Vec::new(),
501 hyperedges: Vec::new(),
502 data: Vec::new(),
503 })
504 }
505
506 fn execute_create_query(&self, _cypher: &str) -> Result<QueryResult, String> {
507 Ok(QueryResult {
511 nodes: Vec::new(),
512 edges: Vec::new(),
513 hyperedges: Vec::new(),
514 data: Vec::new(),
515 })
516 }
517
518 fn generate_hyperedge_embedding(&self, node_ids: &[String]) -> Result<Vec<f32>, JsValue> {
519 let nodes = self.nodes.lock();
520 let embeddings: Vec<Vec<f32>> = node_ids
521 .iter()
522 .filter_map(|id| nodes.get(id).and_then(|n| n.embedding.clone()))
523 .collect();
524
525 if embeddings.is_empty() {
526 return Err(JsValue::from_str("No embeddings found for nodes"));
527 }
528
529 let dim = embeddings[0].len();
530 let mut avg_embedding = vec![0.0; dim];
531
532 for emb in &embeddings {
533 for (i, val) in emb.iter().enumerate() {
534 avg_embedding[i] += val;
535 }
536 }
537
538 for val in &mut avg_embedding {
539 *val /= embeddings.len() as f32;
540 }
541
542 Ok(avg_embedding)
543 }
544}
545
546#[wasm_bindgen]
548pub fn version() -> String {
549 env!("CARGO_PKG_VERSION").to_string()
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use wasm_bindgen_test::*;
556
557 wasm_bindgen_test_configure!(run_in_browser);
558
559 #[wasm_bindgen_test]
560 fn test_version() {
561 assert!(!version().is_empty());
562 }
563
564 #[wasm_bindgen_test]
565 fn test_graph_creation() {
566 let db = GraphDB::new(Some("cosine".to_string())).unwrap();
567 assert!(true); }
569}