nova-boot-graphdb 0.1.1

Graph database helpers and adapters for Nova
Documentation
use crate::{
    NovaGraphDb,
    builders::{CypherQueryBuilder, GraphQlQueryBuilder},
    error::GraphDbError,
    memory::InMemoryGraphStore,
    surreal::{surreal_result_rows, surreal_value_to_node},
    traits::GraphStore,
    types::*,
};
use serde_json::Value as JsonValue;
use std::collections::HashMap;

fn node(id: &str, label: &str) -> GraphNode {
    GraphNode {
        id: id.to_string(),
        labels: vec![label.to_string()],
        properties: HashMap::new(),
    }
}

fn edge(id: &str, from: &str, to: &str, rel_type: &str) -> GraphEdge {
    GraphEdge {
        id: id.to_string(),
        from: from.to_string(),
        to: to.to_string(),
        rel_type: rel_type.to_string(),
        properties: HashMap::new(),
    }
}

#[tokio::test]
async fn traversal_helpers_work_for_in_memory_graph() {
    let graph = NovaGraphDb::in_memory();

    graph
        .upsert_node(node("u1", "User"))
        .await
        .expect("insert node u1");
    graph
        .upsert_node(node("u2", "User"))
        .await
        .expect("insert node u2");
    graph
        .upsert_node(node("u3", "User"))
        .await
        .expect("insert node u3");
    graph
        .upsert_edge(edge("e1", "u1", "u2", "FOLLOWS"))
        .await
        .expect("insert edge e1");
    graph
        .upsert_edge(edge("e2", "u2", "u3", "FOLLOWS"))
        .await
        .expect("insert edge e2");

    let json = graph
        .traverse_json("u1", 2)
        .await
        .expect("traversal should serialize");

    let nodes = json
        .get("nodes")
        .and_then(JsonValue::as_array)
        .expect("nodes array");
    let edges = json
        .get("edges")
        .and_then(JsonValue::as_array)
        .expect("edges array");

    assert_eq!(nodes.len(), 3);
    assert_eq!(edges.len(), 2);
}

#[tokio::test]
async fn in_memory_rejects_empty_node_id() {
    let store = InMemoryGraphStore::default();
    let result = store
        .upsert_node(GraphNode {
            id: String::new(),
            labels: vec!["User".to_string()],
            properties: HashMap::new(),
        })
        .await;

    assert!(matches!(result, Err(GraphDbError::InvalidInput(_))));
}

#[tokio::test]
async fn in_memory_rejects_invalid_edge_input() {
    let store = InMemoryGraphStore::default();

    let empty_id = store
        .upsert_edge(GraphEdge {
            id: String::new(),
            from: "a".to_string(),
            to: "b".to_string(),
            rel_type: "FOLLOWS".to_string(),
            properties: HashMap::new(),
        })
        .await;
    assert!(matches!(empty_id, Err(GraphDbError::InvalidInput(_))));

    let missing_endpoints = store
        .upsert_edge(GraphEdge {
            id: "e1".to_string(),
            from: "a".to_string(),
            to: "b".to_string(),
            rel_type: "FOLLOWS".to_string(),
            properties: HashMap::new(),
        })
        .await;
    assert!(matches!(
        missing_endpoints,
        Err(GraphDbError::InvalidInput(_))
    ));
}

#[tokio::test]
async fn in_memory_execute_is_not_implemented() {
    let store = InMemoryGraphStore::default();
    let result = store
        .execute(GraphQuery::Cypher("RETURN 1".to_string()))
        .await;
    assert!(matches!(result, Err(GraphDbError::NotImplemented(_))));
}

#[test]
fn cypher_builder_produces_expected_query() {
    let q = CypherQueryBuilder::new()
        .match_node("n", "User")
        .where_eq("n", "id", "u1")
        .return_fields("n")
        .build();

    assert_eq!(
        q,
        GraphQuery::Cypher("MATCH (n:User) WHERE n.id = 'u1' RETURN n".to_string())
    );
}

#[test]
fn graphql_builder_produces_expected_query() {
    let q = GraphQlQueryBuilder::new("users")
        .arg("id", "u1")
        .field("id")
        .field("email")
        .build();

    assert_eq!(
        q,
        GraphQuery::GraphQl("query { users(id: \"u1\") { id email } }".to_string())
    );
}

#[tokio::test]
async fn neo4j_adapter_is_constructible() {
    let graph = NovaGraphDb::neo4j("http://127.0.0.1:65535", "neo4j", "pass");
    let result = graph
        .execute(GraphQuery::Cypher("RETURN 1".to_string()))
        .await;
    assert!(matches!(result, Err(GraphDbError::Backend(_))));
}

#[tokio::test]
async fn neo4j_rejects_graphql_query_type() {
    let graph = NovaGraphDb::neo4j("http://127.0.0.1:65535", "neo4j", "pass");
    let result = graph
        .execute(GraphQuery::GraphQl("query { users { id } }".to_string()))
        .await;
    assert!(matches!(result, Err(GraphDbError::InvalidInput(_))));
}

#[tokio::test]
async fn surreal_adapter_is_constructible() {
    let graph = NovaGraphDb::surreal("http://127.0.0.1:65535", "nova", "main");
    let result = graph
        .execute(GraphQuery::GraphQl("query { ping }".to_string()))
        .await;
    assert!(matches!(result, Err(GraphDbError::Backend(_))));
}

#[test]
fn surreal_helpers_parse_result_rows_and_nodes() {
    let payload = serde_json::json!([
        {
            "status": "OK",
            "result": [
                {
                    "neighbors": [
                        {
                            "id": {"tb": "node", "id": "u2"},
                            "properties": {"email": "u2@nova.rs"}
                        },
                        {
                            "id": "node:u3",
                            "properties": {"email": "u3@nova.rs"}
                        }
                    ]
                }
            ]
        }
    ]);

    let rows = surreal_result_rows(&payload);
    assert_eq!(rows.len(), 1);

    let neighbors = rows[0]
        .get("neighbors")
        .and_then(JsonValue::as_array)
        .expect("neighbors should parse");
    assert_eq!(neighbors.len(), 2);

    let n1 = surreal_value_to_node(&neighbors[0]).expect("first node parses");
    let n2 = surreal_value_to_node(&neighbors[1]).expect("second node parses");

    assert_eq!(n1.id, "u2");
    assert_eq!(n2.id, "u3");
}

#[test]
fn surreal_helpers_neighbors_fallback_shape_parses() {
    let payload = serde_json::json!([
        {
            "status": "OK",
            "result": [
                {
                    "neighbors": [
                        {
                            "id": "node:u7",
                            "properties": {"name": "fallback"}
                        }
                    ]
                }
            ]
        }
    ]);

    let rows = surreal_result_rows(&payload);
    let neighbors = rows[0]
        .get("neighbors")
        .and_then(JsonValue::as_array)
        .expect("neighbors should exist");

    let parsed = surreal_value_to_node(&neighbors[0]).expect("fallback neighbor shape parses");
    assert_eq!(parsed.id, "u7");
}

#[tokio::test]
async fn graph_to_json_serializes_traversal_output() {
    let graph = NovaGraphDb::in_memory();
    graph
        .upsert_node(GraphNode {
            id: "s1".to_string(),
            labels: vec!["User".to_string()],
            properties: HashMap::new(),
        })
        .await
        .expect("insert node");

    let json = graph.traverse_json("s1", 1).await.expect("traverse json");
    assert!(json.get("nodes").is_some());
    assert!(json.get("edges").is_some());
}