From 364aea58a120c556f5e7ff1e06e9c3074bbaaad3 Mon Sep 17 00:00:00 2001 From: Shivam Kapoor <4599890+iamsmkr@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:22:58 +0100 Subject: [PATCH] Todos/graphql (#1676) * remove timeout from run and set a sensible default * redo appconfig building * skip graphs that don't have name as property instead of failing * rename graphs names gql to name * remove Graphs hashmap/paths from the RaphtoryServer so all uploads are done via the client * rid expects * impl graph file upload * impl load graph from path * impl overwrite graph * impl namespace * handle more namespace validation cases, add tests * impl get_graph to provide namespace as arg, modified gqlgraphs to hold both names, and namespaces but not graphs saving memory, impl path api on both gqlgraph and gqlgraphs, fixed and added tests * refactor and fix save_graph * impl load_graph_from_path ns * fix issue with dir creation * add rename graph tests, rid saving graph path as graph property, add parent_graph_namespace to save graph and rename graph, add more validation in these apis * add tests for get graphs * add received graph tests * add test for get graph * add test for update last opened * add isarchive tests * fix save and add tests * fix save graph issue * impl create_graph, update_graph and add tests for update graph with new name * impl tests for update graph * add tests for send graph * fix upload graph to accept namespace and add tests * fix load graph and add tests * impl tests for get graph * impl tests for get graphs * fix receive graph versioning issue and add tests * impl rename graph tests * fix archive tests * fix rename graph and add comments * Change graph_nodes to be a vector of string than string, fix issues with send_graph from py and gql, receive graph versioning, refactors * rename renamegraph to movegraph and impl copy graph, add tests for move graph * add tests fro copy graph * impl delete graph gql api and add tests * add properties to nodes and edges to test if they are carried forward when creating new graph with update_graph * fix create graph and add tests * rid dependency of graph name as graph prop, fix tests --- python/Cargo.toml | 2 +- python/src/graphql.rs | 265 +- python/tests/test_graphdb_imports.py | 8 +- python/tests/test_graphql.py | 2548 +++++++++++++++-- raphtory-graphql/src/data.rs | 473 ++- raphtory-graphql/src/lib.rs | 147 +- raphtory-graphql/src/main.rs | 35 +- raphtory-graphql/src/model/graph/graph.rs | 106 +- raphtory-graphql/src/model/graph/graphs.rs | 50 +- raphtory-graphql/src/model/mod.rs | 578 ++-- raphtory-graphql/src/server.rs | 193 +- raphtory-graphql/src/server_config.rs | 237 +- raphtory-graphql/src/url_encode.rs | 27 + raphtory/src/core/utils/errors.rs | 13 +- raphtory/src/db/api/mutation/import_ops.rs | 47 +- .../src/db/api/view/internal/materialize.rs | 36 +- raphtory/src/db/graph/graph.rs | 5 +- raphtory/src/lib.rs | 2 + raphtory/src/python/graph/graph.rs | 31 +- .../src/python/graph/graph_with_deletions.rs | 33 +- 20 files changed, 3756 insertions(+), 1080 deletions(-) create mode 100644 raphtory-graphql/src/url_encode.rs diff --git a/python/Cargo.toml b/python/Cargo.toml index 695ee882b9..3d45257560 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -22,7 +22,7 @@ pyo3 = { workspace = true } raphtory_core = { path = "../raphtory", version = "0.9.1", features = ["python", "search", "vectors"], package = "raphtory" } raphtory-graphql = { path = "../raphtory-graphql", version = "0.9.1" } serde_json = { workspace = true } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["multipart"] } tokio = { workspace = true } crossbeam-channel = { workspace = true } serde = { workspace = true } diff --git a/python/src/graphql.rs b/python/src/graphql.rs index 8334bca67a..724b4f3636 100644 --- a/python/src/graphql.rs +++ b/python/src/graphql.rs @@ -32,18 +32,21 @@ use raphtory_graphql::{ global_plugins::GlobalPlugins, vector_algorithms::VectorAlgorithms, }, server_config::*, - url_encode_graph, RaphtoryServer, + url_encode::url_encode_graph, + RaphtoryServer, }; -use reqwest::Client; +use reqwest::{multipart, multipart::Part, Client}; use serde_json::{json, Map, Number, Value as JsonValue}; use std::{ collections::HashMap, + fs::File, + io::Read, path::{Path, PathBuf}, thread, thread::{sleep, JoinHandle}, time::Duration, }; -use tokio::{self, io::Result as IoResult}; +use tokio::{self, io::Result as IoResult, runtime::Runtime}; /// A class for accessing graphs hosted in a Raphtory GraphQL server and running global search for /// graph documents @@ -140,7 +143,7 @@ impl PyRaphtoryServer { &PathBuf::from(cache), Some(template), ) - .await; + .await?; Ok(Self::new(new_server)) }) } @@ -222,34 +225,40 @@ impl PyRaphtoryServer { impl PyRaphtoryServer { #[new] #[pyo3( - signature = (work_dir, graphs = None, graph_paths = None, cache_capacity = 30, cache_tti_seconds = 900, client_id = None, client_secret = None, tenant_id = None) + signature = (work_dir, cache_capacity = None, cache_tti_seconds = None, client_id = None, client_secret = None, tenant_id = None, log_level = None, config_path = None) )] fn py_new( - work_dir: String, - graphs: Option>, - graph_paths: Option>, - cache_capacity: u64, - cache_tti_seconds: u64, + work_dir: PathBuf, + cache_capacity: Option, + cache_tti_seconds: Option, client_id: Option, client_secret: Option, tenant_id: Option, + log_level: Option, + config_path: Option, ) -> PyResult { - let graph_paths = graph_paths.map(|paths| paths.into_iter().map(PathBuf::from).collect()); - let server = RaphtoryServer::new( - Path::new(&work_dir), - graphs, - graph_paths, - Some(CacheConfig { - capacity: cache_capacity, - tti_seconds: cache_tti_seconds, - }), - Some(AuthConfig { - client_id, - client_secret, - tenant_id, - }), - None, - ); + let mut app_config_builder = AppConfigBuilder::new(); + if let Some(log_level) = log_level { + app_config_builder = app_config_builder.with_log_level(log_level); + } + if let Some(cache_capacity) = cache_capacity { + app_config_builder = app_config_builder.with_cache_capacity(cache_capacity); + } + if let Some(cache_tti_seconds) = cache_tti_seconds { + app_config_builder = app_config_builder.with_cache_tti_seconds(cache_tti_seconds); + } + if let Some(client_id) = client_id { + app_config_builder = app_config_builder.with_auth_client_id(client_id); + } + if let Some(client_secret) = client_secret { + app_config_builder = app_config_builder.with_auth_client_secret(client_secret); + } + if let Some(tenant_id) = tenant_id { + app_config_builder = app_config_builder.with_auth_tenant_id(tenant_id); + } + let app_config = Some(app_config_builder.build()); + + let server = RaphtoryServer::new(work_dir, app_config, config_path)?; Ok(PyRaphtoryServer::new(server)) } @@ -364,13 +373,12 @@ impl PyRaphtoryServer { /// * `enable_auth`: enable authentication (defaults to False). /// * `timeout_in_milliseconds`: wait for server to be online (defaults to 5000). The server is stopped if not online within timeout_in_milliseconds but manages to come online as soon as timeout_in_milliseconds finishes! #[pyo3( - signature = (port = 1736, log_level = "INFO".to_string(), enable_tracing = false, enable_auth = false, timeout_in_milliseconds = None) + signature = (port = 1736, enable_tracing = false, enable_auth = false, timeout_in_milliseconds = None) )] pub fn start( slf: PyRefMut, py: Python, port: u16, - log_level: String, enable_tracing: bool, enable_auth: bool, timeout_in_milliseconds: Option, @@ -386,9 +394,8 @@ impl PyRaphtoryServer { .build() .unwrap() .block_on(async move { - let handler = - server.start_with_port(port, Some(&log_level), enable_tracing, enable_auth); - let running_server = handler.await; + let handler = server.start_with_port(port, enable_tracing, enable_auth); + let running_server = handler.await?; let tokio_sender = running_server._get_sender().clone(); tokio::task::spawn_blocking(move || { match receiver.recv().expect("Failed to wait for cancellation") { @@ -426,27 +433,17 @@ impl PyRaphtoryServer { /// Arguments: /// * `port`: the port to use (defaults to 1736). #[pyo3( - signature = (port = 1736, log_level = "INFO".to_string(), enable_tracing = false, enable_auth = false, timeout_in_milliseconds = None) + signature = (port = 1736, enable_tracing = false, enable_auth = false) )] pub fn run( slf: PyRefMut, py: Python, port: u16, - log_level: String, enable_tracing: bool, enable_auth: bool, - timeout_in_milliseconds: Option, ) -> PyResult<()> { - let mut server = Self::start( - slf, - py, - port, - log_level, - enable_tracing, - enable_auth, - timeout_in_milliseconds, - )? - .server_handler; + let mut server = + Self::start(slf, py, port, enable_tracing, enable_auth, Some(180000))?.server_handler; py.allow_threads(|| wait_server(&mut server)) } } @@ -464,7 +461,7 @@ fn adapt_graphql_value(value: &ValueAccessor, py: Python) -> PyObject { } GraphqlValue::String(value) => value.to_object(py), GraphqlValue::Boolean(value) => value.to_object(py), - value => panic!("graphql input value {value} has an unsuported type"), + value => panic!("graphql input value {value} has an unsupported type"), } } @@ -642,30 +639,6 @@ impl PyRaphtoryClient { .map_err(|err| adapt_err_value(&err)) .map(|json| (request_body, json)) } - - fn load_graphs( - &self, - py: Python, - path: String, - overwrite: bool, - ) -> PyResult> { - let query = - format!("mutation {{ loadGraphsFromPath(path: \"{path}\", overwrite: {overwrite}) }}"); - let variables = []; - - let data = self.query_with_json_variables(query.clone(), variables.into())?; - - match data.get("loadGraphsFromPath") { - Some(JsonValue::Array(loads)) => { - let num_graphs = loads.len(); - println!("Loaded {num_graphs} graph(s)"); - translate_map_to_python(py, data) - } - _ => Err(PyException::new_err(format!( - "Error while reading server response for query:\n\t{query}\nGot data:\n\t'{data:?}'" - ))), - } - } } const WAIT_CHECK_INTERVAL_MILLIS: u64 = 200; @@ -710,31 +683,38 @@ impl PyRaphtoryClient { translate_map_to_python(py, data) } - /// Send a graph to the server. + /// Send a graph to the server /// /// Arguments: - /// * `name`: the name of the graph sent. - /// * `graph`: the graph to send. + /// * `name`: the name of the graph + /// * `graph`: the graph to send + /// * `overwrite`: overwrite existing graph (defaults to False) + /// * `namespace`: the namespace of the graph /// /// Returns: /// The `data` field from the graphQL response after executing the mutation. + #[pyo3(signature = (name, graph, overwrite = false, namespace = None))] fn send_graph( &self, py: Python, name: String, graph: MaterializedGraph, + overwrite: bool, + namespace: Option, ) -> PyResult> { let encoded_graph = encode_graph(graph)?; let query = r#" - mutation SendGraph($name: String!, $graph: String!) { - sendGraph(name: $name, graph: $graph) + mutation SendGraph($name: String!, $graph: String!, $overwrite: Boolean!, $namespace: String) { + sendGraph(name: $name, graph: $graph, overwrite: $overwrite, namespace: $namespace) } "# - .to_owned(); + .to_owned(); let variables = [ ("name".to_owned(), json!(name)), ("graph".to_owned(), json!(encoded_graph)), + ("overwrite".to_owned(), json!(overwrite)), + ("namespace".to_owned(), json!(namespace)), ]; let data = self.query_with_json_variables(query, variables.into())?; @@ -751,22 +731,143 @@ impl PyRaphtoryClient { } } - /// Set the server to load all the graphs from its path `path`. + /// Upload graph file from a path `file_path` on the client + /// + /// Arguments: + /// * `name`: the name of the graph + /// * `file_path`: the path of the graph on the client + /// * `overwrite`: overwrite existing graph (defaults to False) + /// * `namespace`: the namespace of the graph + /// + /// Returns: + /// The `data` field from the graphQL response after executing the mutation. + #[pyo3(signature = (name, file_path, overwrite = false, namespace = None))] + fn upload_graph( + &self, + py: Python, + name: String, + file_path: String, + overwrite: bool, + namespace: Option, + ) -> PyResult> { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let client = Client::new(); + + let mut file = File::open(Path::new(&file_path)).map_err(|err| adapt_err_value(&err))?; + + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).map_err(|err| adapt_err_value(&err))?; + + let mut variables = format!( + r#""name": "{}", "overwrite": {}, "graph": null"#, + name, overwrite + ); + + if let Some(ns) = &namespace { + variables = format!(r#""namespace": "{}", {}"#, ns, variables); + } + + let operations = format!( + r#"{{ + "query": "mutation UploadGraph($name: String!, $graph: Upload!, $overwrite: Boolean!{}) {{ uploadGraph(name: $name, graph: $graph, overwrite: $overwrite{}) }}", + "variables": {{ {} }} + }}"#, + if namespace.is_some() { ", $namespace: String" } else { "" }, + if namespace.is_some() { ", namespace: $namespace" } else { "" }, + variables + ); + + let form = multipart::Form::new() + .text("operations", operations) + .text("map", r#"{"0": ["variables.graph"]}"#) + .part("0", Part::bytes(buffer).file_name(file_path.clone())); + + let response = client + .post(&self.url) + .multipart(form) + .send() + .await + .map_err(|err| adapt_err_value(&err))?; + + let status = response.status(); + let text = response.text().await.map_err(|err| adapt_err_value(&err))?; + + if !status.is_success() { + return Err(PyException::new_err(format!( + "Error Uploading Graph. Status: {}. Response: {}", + status, text + ))); + } + + let mut data: HashMap = serde_json::from_str(&text).map_err(|err| { + PyException::new_err(format!( + "Failed to parse JSON response: {}. Response text: {}", + err, text + )) + })?; + + match data.remove("data") { + Some(JsonValue::Object(data)) => { + let mut result_map = HashMap::new(); + for (key, value) in data { + result_map.insert(key, translate_to_python(py, value)?); + } + Ok(result_map) + } + _ => match data.remove("errors") { + Some(JsonValue::Array(errors)) => Err(PyException::new_err(format!( + "Error Uploading Graph. Got errors:\n\t{:#?}", + errors + ))), + _ => Err(PyException::new_err(format!( + "Error Uploading Graph. Unexpected response: {}", + text + ))), + }, + } + }) + } + + /// Load graph from a path `path` on the server. /// /// Arguments: - /// * `path`: the path to load the graphs from. - /// * `overwrite`: overwrite existing graphs (defaults to False) + /// * `file_path`: the path to load the graph from. + /// * `overwrite`: overwrite existing graph (defaults to False) /// /// Returns: /// The `data` field from the graphQL response after executing the mutation. - #[pyo3(signature = (path, overwrite = false))] - fn load_graphs_from_path( + #[pyo3(signature = (file_path, overwrite = false, namespace = None))] + fn load_graph( &self, py: Python, - path: String, + file_path: String, overwrite: bool, + namespace: Option, ) -> PyResult> { - self.load_graphs(py, path, overwrite) + let query = r#" + mutation LoadGraph($file_path: String!, $overwrite: Boolean!, $namespace: String) { + loadGraphFromPath(filePath: $file_path, overwrite: $overwrite, namespace: $namespace) + } + "# + .to_owned(); + let variables = [ + ("file_path".to_owned(), json!(file_path)), + ("overwrite".to_owned(), json!(overwrite)), + ("namespace".to_owned(), json!(namespace)), + ]; + + let data = self.query_with_json_variables(query.clone(), variables.into())?; + + match data.get("loadGraphFromPath") { + Some(JsonValue::String(name)) => { + println!("Loaded graph: '{name}'"); + translate_map_to_python(py, data) + } + _ => Err(PyException::new_err(format!( + "Error while reading server response for query:\n\t{query}\nGot data:\n\t'{data:?}'" + ))), + } } } diff --git a/python/tests/test_graphdb_imports.py b/python/tests/test_graphdb_imports.py index e08e570008..f6652d9cc9 100644 --- a/python/tests/test_graphdb_imports.py +++ b/python/tests/test_graphdb_imports.py @@ -20,10 +20,9 @@ def test_import_into_graph(): assert res.properties.constant.get("con") == 11 gg = Graph() - res = gg.import_nodes([g_a, g_b]) - assert len(res) == 2 + gg.import_nodes([g_a, g_b]) assert len(gg.nodes) == 2 - assert [x.name for x in res] == ["A", "B"] + assert [x.name for x in gg.nodes] == ["A", "B"] e_a_b = g.add_edge(2, "A", "B") res = gg.import_edge(e_a_b) @@ -37,7 +36,6 @@ def test_import_into_graph(): e_c_d = g.add_edge(4, "C", "D") gg = Graph() - res = gg.import_edges([e_a_b, e_c_d]) - assert len(res) == 2 + gg.import_edges([e_a_b, e_c_d]) assert len(gg.nodes) == 4 assert len(gg.edges) == 2 diff --git a/python/tests/test_graphql.py b/python/tests/test_graphql.py index 6cf0bc51b5..9f3b77274b 100644 --- a/python/tests/test_graphql.py +++ b/python/tests/test_graphql.py @@ -1,226 +1,2435 @@ +import base64 +import os import tempfile +import time + from raphtory.graphql import RaphtoryServer, RaphtoryClient from raphtory import graph_loader from raphtory import Graph import json -def test_failed_server_start_in_time(): - tmp_work_dir = tempfile.mkdtemp() - server = None +def test_failed_server_start_in_time(): + tmp_work_dir = tempfile.mkdtemp() + server = None + try: + server = RaphtoryServer(tmp_work_dir).start(timeout_in_milliseconds=1) + except Exception as e: + assert str(e) == "Failed to start server in 1 milliseconds" + finally: + if server: + server.stop() + + +def test_successful_server_start_in_time(): + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start(timeout_in_milliseconds=3000) + client = server.get_client() + assert client.is_server_online() + server.stop() + assert not client.is_server_online() + + +def test_server_start_on_default_port(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.send_graph(name="g", graph=g) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_server_start_on_custom_port(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start(port=1737) + client = RaphtoryClient("http://localhost:1737") + client.send_graph(name="g", graph=g) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_send_graph_succeeds_if_no_graph_found_with_same_name(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.send_graph(name="g", graph=g) + + server.stop() + + +def test_send_graph_fails_if_graph_already_exists(): + tmp_work_dir = tempfile.mkdtemp() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.save_to_file(os.path.join(tmp_work_dir, "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.send_graph(name="g", graph=g) + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_send_graph_succeeds_if_graph_already_exists_with_overwrite_enabled(): + tmp_work_dir = tempfile.mkdtemp() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.save_to_file(os.path.join(tmp_work_dir, "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + client.send_graph(name="g", graph=g, overwrite=True) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +def test_send_graph_succeeds_if_no_graph_found_with_same_name_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.send_graph(name="g", graph=g, namespace="shivam") + + server.stop() + + +def test_send_graph_fails_if_graph_already_exists_at_namespace(): + tmp_work_dir = tempfile.mkdtemp() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.send_graph(name="g", graph=g, namespace="shivam") + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_send_graph_succeeds_if_graph_already_exists_at_namespace_with_overwrite_enabled(): + tmp_work_dir = tempfile.mkdtemp() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + client.send_graph(name="g", graph=g, overwrite=True, namespace="shivam") + + query = """{graph(name: "g", namespace: "shivam") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +def test_namespaces(): + def assert_graph_fetch(name, namespace): + query = f"""{{ graph(name: "{name}", namespace: "{namespace}") {{ nodes {{ list {{ name }} }} }} }}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + name = "g" + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Default namespace, graph is saved in the work dir + client.send_graph(name=name, graph=g, overwrite=True) + expected_path = os.path.join(tmp_work_dir, name) + assert os.path.exists(expected_path) + + namespace = "shivam" + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + expected_path = os.path.join(tmp_work_dir, namespace, name) + assert os.path.exists(expected_path) + assert_graph_fetch(name, namespace) + + namespace = "./shivam/investigation" + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + expected_path = os.path.join(tmp_work_dir, namespace, name) + assert os.path.exists(expected_path) + assert_graph_fetch(name, namespace) + + namespace = "./shivam/investigation/2024/12/12" + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + expected_path = os.path.join(tmp_work_dir, namespace, name) + assert os.path.exists(expected_path) + assert_graph_fetch(name, namespace) + + namespace = "shivam/investigation/2024-12-12" + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + expected_path = os.path.join(tmp_work_dir, namespace, name) + assert os.path.exists(expected_path) + assert_graph_fetch(name, namespace) + + namespace = "../shivam" + try: + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + except Exception as e: + assert "Invalid namespace: ../shivam" in str(e), f"Unexpected exception message: {e}" + + namespace = "./shivam/../investigation" + try: + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + except Exception as e: + assert "Invalid namespace: ./shivam/../investigation" in str(e), f"Unexpected exception message: {e}" + + namespace = "//shivam/investigation" + try: + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + except Exception as e: + assert "//shivam/investigation" in str(e), f"Unexpected exception message: {e}" + + namespace = "shivam/investigation//2024-12-12" + try: + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + except Exception as e: + assert "shivam/investigation//2024-12-12" in str(e), f"Unexpected exception message: {e}" + + namespace = "shivam/investigation\2024-12-12" + try: + client.send_graph(name=name, graph=g, overwrite=True, namespace=namespace) + except Exception as e: + assert "shivam/investigation\2024-12-12" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +# Test upload graph +def test_upload_graph_succeeds_if_no_graph_found_with_same_name(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.upload_graph(name="g", file_path=g_file_path, overwrite=False) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_upload_graph_fails_if_graph_already_exists(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + g.save_to_file(os.path.join(tmp_work_dir, "g")) + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.upload_graph(name="g", file_path=g_file_path) + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_upload_graph_succeeds_if_graph_already_exists_with_overwrite_enabled(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + client.upload_graph(name="g", file_path=g_file_path, overwrite=True) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +# Test upload graph at namespace +def test_upload_graph_succeeds_if_no_graph_found_with_same_name_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.upload_graph(name="g", file_path=g_file_path, overwrite=False, namespace="shivam") + + query = """{graph(name: "g", namespace: "shivam") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_upload_graph_fails_if_graph_already_exists_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.upload_graph(name="g", file_path=g_file_path, overwrite=False, namespace="shivam") + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_upload_graph_succeeds_if_graph_already_exists_at_namespace_with_overwrite_enabled(): + tmp_work_dir = tempfile.mkdtemp() + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + client.upload_graph(name="g", file_path=g_file_path, overwrite=True, namespace="shivam") + + query = """{graph(name: "g", namespace: "shivam") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +def test_load_graph_succeeds_if_no_graph_found_with_same_name(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.load_graph(file_path=g_file_path, overwrite=False) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_load_graph_fails_if_graph_already_exists(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + g.save_to_file(os.path.join(tmp_work_dir, "g")) + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.load_graph(file_path=g_file_path) + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_load_graph_succeeds_if_graph_already_exists_with_overwrite_enabled(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + client.load_graph(file_path=g_file_path, overwrite=True) + + query = """{graph(name: "g") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +# Test load graph at namespace +def test_load_graph_succeeds_if_no_graph_found_with_same_name_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + client.load_graph(file_path=g_file_path, overwrite=False, namespace="shivam") + + query = """{graph(name: "g", namespace: "shivam") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + } + + server.stop() + + +def test_load_graph_fails_if_graph_already_exists_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + tmp_work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + try: + client.load_graph(file_path=g_file_path, overwrite=False, namespace="shivam") + except Exception as e: + assert "Graph already exists by name = g" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_load_graph_succeeds_if_graph_already_exists_at_namespace_with_overwrite_enabled(): + tmp_work_dir = tempfile.mkdtemp() + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + os.makedirs(os.path.join(tmp_work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(tmp_work_dir, "shivam", "g")) + + server = RaphtoryServer(tmp_work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + g.add_edge(4, "ben", "shivam") + tmp_dir = tempfile.mkdtemp() + g_file_path = tmp_dir + "/g" + g.save_to_file(g_file_path) + + client.load_graph(file_path=g_file_path, overwrite=True, namespace="shivam") + + query = """{graph(name: "g", namespace: "shivam") {nodes {list {name}}}}""" + assert client.query(query) == { + "graph": { + "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}, {"name": "shivam"}]} + } + } + + server.stop() + + +def test_get_graph_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """{ graph(name: "g1") { name, path, nodes { list { name } } } }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found g1" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_get_graph_fails_if_graph_not_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """{ graph(name: "g1", namespace: "shivam") { name, path, nodes { list { name } } } }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g1" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_get_graph_succeeds_if_graph_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + + query = """{ graph(name: "g1") { name, path, nodes { list { name } } } }""" + assert client.query(query) == { + 'graph': {'name': 'g1', 'nodes': {'list': [{'name': 'ben'}, {'name': 'hamza'}, {'name': 'haaroon'}]}, + 'path': 'g1'}} + + server.stop() + + +def test_get_graph_succeeds_if_graph_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + query = """{ graph(name: "g2", namespace: "shivam") { name, path, nodes { list { name } } } }""" + assert client.query(query) == { + 'graph': {'name': 'g2', 'nodes': {'list': [{'name': 'ben'}, {'name': 'hamza'}, {'name': 'haaroon'}]}, + 'path': 'shivam/g2'}} + + server.stop() + + +def test_get_graphs_returns_emtpy_list_if_no_graphs_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + # Assert if no graphs are discoverable + query = """{ graphs { name, path } }""" + assert client.query(query) == { + 'graphs': {'name': [], 'path': []} + } + + server.stop() + + +def test_get_graphs_returns_graph_list_if_graphs_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + # Assert if all graphs present in the work_dir are discoverable + query = """{ graphs { name, path } }""" + assert client.query(query) == { + 'graphs': {'name': ['g1', 'g2', 'g3'], 'path': ['g1', 'shivam/g2', 'shivam/g3']} + } + + server.stop() + + +def test_receive_graph_fails_if_no_graph_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """{ receiveGraph(name: "g2") }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found g2" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_receive_graph_succeeds_if_graph_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + g.save_to_file(os.path.join(work_dir, "g1")) + + received_graph = 'AQAAAAAAAAADAAAAAAAAAJTXBAscINjlAQAAAAAAAADv0+QnEcpEzAIAAAAAAAAArUhReOzDmFAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABAAAAAAAAAK1IUXjsw5hQAQMAAAAAAAAAYmVuAAAAAAAAAAACAAAAAgAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAABAAAAAgAAAAIAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAJTXBAscINjlAQUAAAAAAAAAaGFtemEBAAAAAAAAAAIAAAACAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA79PkJxHKRMwBBwAAAAAAAABoYWFyb29uAgAAAAAAAAACAAAAAgAAAAAAAAACAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAABAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAEAAAAAAAAAAQAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAQAAAAAAAAABAAAAAwAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAACQAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQBAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQAAAAAAAAAAAEAAAAAAAAACAAAAAAAAABfZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAABfZGVmYXVsdAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + + query = """{ receiveGraph(name: "g1") }""" + assert client.query(query) == { + 'receiveGraph': received_graph + } + + decoded_bytes = base64.b64decode(received_graph) + + g = Graph.from_bincode(decoded_bytes) + assert g.nodes.name == ["ben", "hamza", "haaroon"] + + server.stop() + + +def test_receive_graph_fails_if_no_graph_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """{ receiveGraph(name: "g2", namespace: "shivam") }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g2" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_receive_graph_succeeds_if_graph_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + received_graph = 'AQAAAAAAAAADAAAAAAAAAJTXBAscINjlAQAAAAAAAADv0+QnEcpEzAIAAAAAAAAArUhReOzDmFAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAABAAAAAAAAAK1IUXjsw5hQAQMAAAAAAAAAYmVuAAAAAAAAAAACAAAAAgAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAABAAAAAgAAAAIAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAJTXBAscINjlAQUAAAAAAAAAaGFtemEBAAAAAAAAAAIAAAACAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAgAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA79PkJxHKRMwBBwAAAAAAAABoYWFyb29uAgAAAAAAAAACAAAAAgAAAAAAAAACAAAAAAAAAAMAAAAAAAAAAQAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAABAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAEAAAAAAAAAAQAAAAIAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAQAAAAAAAAAAAQAAAAAAAAABAAAAAwAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAACQAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQBAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQAAAAAAAAAAAEAAAAAAAAACAAAAAAAAABfZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAABfZGVmYXVsdAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AQAAAAAAAAAIAAAAAAAAAF9kZWZhdWx0AAAAAAAAAAABAAAAAAAAAAgAAAAAAAAAX2RlZmF1bHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + + query = """{ receiveGraph(name: "g2", namespace: "shivam") }""" + assert client.query(query) == { + 'receiveGraph': received_graph + } + + decoded_bytes = base64.b64decode(received_graph) + + g = Graph.from_bincode(decoded_bytes) + assert g.nodes.name == ["ben", "hamza", "haaroon"] + + server.stop() + + +def test_move_graph_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + moveGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found ben/g5" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_move_graph_fails_if_graph_with_same_name_already_exists(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + moveGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_move_graph_fails_if_graph_with_same_name_already_exists_at_same_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "ben", "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + moveGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + newGraphNamespace: "ben", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = ben/g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_move_graph_fails_if_graph_with_same_name_already_exists_at_diff_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "shivam", "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + moveGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + newGraphNamespace: "shivam", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = shivam/g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_move_graph_succeeds(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if rename graph succeeds and old graph is deleted + query = """mutation { + moveGraph( + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g4", + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "shivam") {nodes {list {name}}}}""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g3" in str(e), f"Unexpected exception message: {e}" + + query = """{graph(name: "g4") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_move_graph_succeeds_at_same_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if rename graph succeeds and old graph is deleted + query = """mutation { + moveGraph( + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g4", + newGraphNamespace: "shivam" + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "shivam") {nodes {list {name}}}}""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g3" in str(e), f"Unexpected exception message: {e}" + + query = """{graph(name: "g4", namespace: "shivam") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_move_graph_succeeds_at_diff_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "ben", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if rename graph succeeds and old graph is deleted + query = """mutation { + moveGraph( + graphName: "g3", + graphNamespace: "ben", + newGraphName: "g4", + newGraphNamespace: "shivam", + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "ben") {nodes {list {name}}}}""" + try: + client.query(query) + except Exception as e: + assert "Graph not found ben/g3" in str(e), f"Unexpected exception message: {e}" + + query = """{graph(name: "g4", namespace: "shivam") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_copy_graph_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + copyGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found ben/g5" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_copy_graph_fails_if_graph_with_same_name_already_exists(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + copyGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_copy_graph_fails_if_graph_with_same_name_already_exists_at_same_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "ben", "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + copyGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + newGraphNamespace: "ben", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = ben/g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_copy_graph_fails_if_graph_with_same_name_already_exists_at_diff_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "ben", "g5")) + g.save_to_file(os.path.join(work_dir, "shivam", "g6")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + copyGraph( + graphName: "g5", + graphNamespace: "ben", + newGraphName: "g6", + newGraphNamespace: "shivam", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = shivam/g6" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_copy_graph_succeeds(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if copy graph succeeds and old graph is retained + query = """mutation { + copyGraph( + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g4", + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "shivam") { nodes {list {name}} }}""" + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + + query = """{graph(name: "g4") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_copy_graph_succeeds_at_same_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if rename graph succeeds and old graph is deleted + query = """mutation { + copyGraph( + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g4", + newGraphNamespace: "shivam" + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "shivam") { nodes {list {name}} }}""" + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + + query = """{graph(name: "g4", namespace: "shivam") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_copy_graph_succeeds_at_diff_namespace_as_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "ben"), exist_ok=True) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "ben", "g3")) + + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + # Assert if rename graph succeeds and old graph is deleted + query = """mutation { + copyGraph( + graphName: "g3", + graphNamespace: "ben", + newGraphName: "g4", + newGraphNamespace: "shivam", + ) + }""" + client.query(query) + + query = """{graph(name: "g3", namespace: "ben") { nodes {list {name}} }}""" + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + + query = """{graph(name: "g4", namespace: "shivam") { + nodes {list {name}} + properties { + constant { + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + } + } + }}""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [{'name': 'ben'}, {"name": "hamza"}, {'name': 'haaroon'}] + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + + server.stop() + + +def test_delete_graph_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + query = """mutation { + deleteGraph( + graphName: "g5", + graphNamespace: "ben", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found ben/g5" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_delete_graph_succeeds_if_graph_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + g.save_to_file(os.path.join(work_dir, "g1")) + + query = """mutation { + deleteGraph( + graphName: "g1", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found ben/g5" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_delete_graph_succeeds_if_graph_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = RaphtoryClient("http://localhost:1736") + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g1")) + + query = """mutation { + deleteGraph( + graphName: "g1", + graphNamespace: "shivam", + ) + }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g1" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_create_graph_fail_if_parent_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + createGraph( + parentGraphName: "g0", + newGraphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_create_graph_fail_if_parent_graph_not_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + createGraph( + parentGraphNamespace: "shivam", + parentGraphName: "g0", + newGraphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_create_graph_fail_if_graph_already_exists(): + work_dir = tempfile.mkdtemp() + + g = Graph() + g.save_to_file(os.path.join(work_dir, "g0")) + g.save_to_file(os.path.join(work_dir, "g3")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + createGraph( + parentGraphName: "g0", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = g3" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_create_graph_fail_if_graph_already_exists_at_namespace(): + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g = Graph() + g.save_to_file(os.path.join(work_dir, "g0")) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + createGraph( + parentGraphName: "g0", + newGraphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph already exists by name = shivam/g3" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_create_graph_succeeds(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + + g.save_to_file(os.path.join(work_dir, "g1")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + createGraph( + parentGraphName: "g1", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +def test_create_graph_succeeds_at_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + + g.save_to_file(os.path.join(work_dir, "g1")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + createGraph( + parentGraphName: "g1", + newGraphNamespace: "shivam", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +# Update Graph with new graph name tests (save as new graph name) +def test_update_graph_with_new_graph_name_fails_if_parent_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + updateGraph( + parentGraphName: "g0", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_update_graph_with_new_graph_name_fails_if_current_graph_not_found(): + g = Graph() + work_dir = tempfile.mkdtemp() + g.save_to_file(os.path.join(work_dir, "g1")) + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g0", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_update_graph_with_new_graph_name_fails_if_new_graph_already_exists(): + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + try: - server = RaphtoryServer(tmp_work_dir).start(timeout_in_milliseconds=1) + client.query(query) except Exception as e: - assert str(e) == "Failed to start server in 1 milliseconds" - finally: - if server: - server.stop() + assert "Graph already exists by name = shivam/g3" in str(e), f"Unexpected exception message: {e}" + server.stop() -def test_successful_server_start_in_time(): - tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir).start(timeout_in_milliseconds=3000) + +def test_update_graph_with_new_graph_name_succeeds_if_parent_graph_belongs_to_different_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() client = server.get_client() - assert client.is_server_online() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + server.stop() - assert not client.is_server_online() -def test_server_start_on_default_port(): +def test_update_graph_with_new_graph_name_succeeds_if_parent_graph_belongs_to_same_namespace(): g = Graph() - g.add_edge(1, "ben", "hamza") - g.add_edge(2, "haaroon", "hamza") - g.add_edge(3, "ben", "haaroon") - - graphs = {"g": g} - tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir, graphs=graphs).start() - client = RaphtoryClient("http://localhost:1736") + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) - query = """{graph(name: "g") {nodes {list {name}}}}""" - assert client.query(query) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g2", + parentGraphNamespace: "shivam", + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g5", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g5", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} } - } - + }""" + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + server.stop() -def test_server_start_on_custom_port(): +def test_update_graph_with_new_graph_name_succeeds_with_new_node_from_parent_graph_added_to_new_graph(): + work_dir = tempfile.mkdtemp() g = Graph() - g.add_edge(1, "ben", "hamza") - g.add_edge(2, "haaroon", "hamza") - g.add_edge(3, "ben", "haaroon") - - graphs = {"g": g} - tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir, graphs=graphs).start(port=1737) - client = RaphtoryClient("http://localhost:1737") - query = """{graph(name: "g") {nodes {list {name}}}}""" - assert client.query(query) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_edge(4, "ben", "shivam", {"prop1": 4}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + g.add_node(7, "shivam", {"dept": "engineering"}) + g.save_to_file(os.path.join(work_dir, "g1")) + + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "shivam"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} } - } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'shivam', 'properties': {'temporal': {'get': {'values': ['engineering']}}}} + ] + assert result['graph']['edges']['list'] == [] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 server.stop() - -def test_load_graphs_from_graph_paths_when_starting_server(): - g1 = Graph() - g1.add_edge(1, "ben", "hamza") - g1.add_edge(2, "haaroon", "hamza") - g1.add_edge(3, "ben", "haaroon") - g1_file_path = tempfile.mkdtemp() + "/g1" - g1.save_to_file(g1_file_path) - g2 = Graph() - g2.add_edge(1, "Naomi", "Shivam") - g2.add_edge(2, "Shivam", "Pedro") - g2.add_edge(3, "Pedro", "Rachel") - g2_file_path = tempfile.mkdtemp() + "/g2" - g2.save_to_file(g2_file_path) - tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir, graph_paths=[g1_file_path, g2_file_path]).start() +def test_update_graph_with_new_graph_name_succeeds_with_new_node_removed_from_new_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() client = server.get_client() - query_g1 = """{graph(name: "g1") {nodes {list {name}}}}""" - query_g2 = """{graph(name: "g2") {nodes {list {name}}}}""" - assert client.query(query_g1) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} } - } - assert client.query(query_g2) == { - "graph": { - "nodes": { - "list": [ - {"name": "Naomi"}, - {"name": "Shivam"}, - {"name": "Pedro"}, - {"name": "Rachel"}, - ] - } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['creationTime']['value'] is not None + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +# Update Graph tests (save graph as same graph name) +def test_update_graph_fails_if_parent_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + updateGraph( + parentGraphName: "g0", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g2", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_update_graph_fails_if_current_graph_not_found(): + g = Graph() + work_dir = tempfile.mkdtemp() + g.save_to_file(os.path.join(work_dir, "g1")) + server = RaphtoryServer(work_dir).start() + client = server.get_client() + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g0", + graphNamespace: "shivam", + newGraphName: "g0", + props: "{{ \\"target\\": 6 : }}", + isArchive: 0, + graphNodes: ["ben"] + ) + }""" + + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g0" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_update_graph_succeeds_if_parent_graph_belongs_to_different_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g2", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g2", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} } - } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +def test_update_graph_succeeds_if_parent_graph_belongs_to_same_namespace(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + g.save_to_file(os.path.join(work_dir, "shivam", "g3")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g2", + parentGraphNamespace: "shivam", + graphName: "g3", + graphNamespace: "shivam", + newGraphName: "g3", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "hamza"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g3", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'hamza', 'properties': {'temporal': {'get': {'values': ['director']}}}} + ] + assert result['graph']['edges']['list'] == [{'properties': {'temporal': {'get': {'values': ['1']}}}}] + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +def test_update_graph_succeeds_with_new_node_from_parent_graph_added_to_new_graph(): + work_dir = tempfile.mkdtemp() + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_edge(4, "ben", "shivam", {"prop1": 4}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + g.add_node(7, "shivam", {"dept": "engineering"}) + g.save_to_file(os.path.join(work_dir, "g1")) + + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g2", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben", "shivam"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g2", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + {'name': 'shivam', 'properties': {'temporal': {'get': {'values': ['engineering']}}}} + ] + assert result['graph']['edges']['list'] == [] + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +def test_update_graph_succeeds_with_new_node_removed_from_new_graph(): + g = Graph() + g.add_edge(1, "ben", "hamza", {"prop1": 1}) + g.add_edge(2, "haaroon", "hamza", {"prop1": 2}) + g.add_edge(3, "ben", "haaroon", {"prop1": 3}) + g.add_node(4, "ben", {"dept": "engineering"}) + g.add_node(5, "hamza", {"dept": "director"}) + g.add_node(6, "haaroon", {"dept": "operations"}) + + work_dir = tempfile.mkdtemp() + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { + updateGraph( + parentGraphName: "g1", + graphName: "g2", + graphNamespace: "shivam", + newGraphName: "g2", + props: "{ \\"target\\": 6 : }", + isArchive: 1, + graphNodes: ["ben"] + ) + }""" + client.query(query) + + query = """{ + graph(name: "g2", namespace: "shivam") { + nodes {list { + name + properties { temporal { get(key: "dept") { values } } } + }} + edges { list { + properties { temporal { get(key: "prop1") { values } } } + }} + properties { constant { + creationTime: get(key: "creationTime") { value } + lastUpdated: get(key: "lastUpdated") { value } + lastOpened: get(key: "lastOpened") { value } + uiProps: get(key: "uiProps") { value } + isArchive: get(key: "isArchive") { value } + }} + } + }""" + + result = client.query(query) + assert result['graph']['nodes']['list'] == [ + {'name': 'ben', 'properties': {'temporal': {'get': {'values': ['engineering']}}}}, + ] + assert result['graph']['edges']['list'] == [] + assert result['graph']['properties']['constant']['lastOpened']['value'] is not None + assert result['graph']['properties']['constant']['lastUpdated']['value'] is not None + assert result['graph']['properties']['constant']['uiProps']['value'] == '{ "target": 6 : }' + assert result['graph']['properties']['constant']['isArchive']['value'] == 1 + + server.stop() + + +def test_update_graph_last_opened_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { updateGraphLastOpened(graphName: "g1") }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found g1" in str(e), f"Unexpected exception message: {e}" server.stop() -def test_send_graphs_to_server(): +def test_update_graph_last_opened_fails_if_graph_not_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + query = """mutation { updateGraphLastOpened(graphName: "g1", namespace: "shivam") }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g1" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_update_graph_last_opened_succeeds(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + g = Graph() g.add_edge(1, "ben", "hamza") g.add_edge(2, "haaroon", "hamza") g.add_edge(3, "ben", "haaroon") - - tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir).start() - client = RaphtoryClient("http://localhost:1736") - client.send_graph(name="g", graph=g) - query = """{graph(name: "g") {nodes {list {name}}}}""" - assert client.query(query) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} - } - } + g.save_to_file(os.path.join(work_dir, "g1")) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + query_last_opened = """{ graph(name: "g1") { properties { constant { get(key: "lastOpened") { value } } } } }""" + mutate_last_opened = """mutation { updateGraphLastOpened(graphName: "g1") }""" + assert client.query(query_last_opened) == {'graph': {'properties': {'constant': {'get': None}}}} + assert client.query(mutate_last_opened) == {'updateGraphLastOpened': True} + updated_last_opened1 = client.query(query_last_opened)['graph']['properties']['constant']['get']['value'] + time.sleep(1) + assert client.query(mutate_last_opened) == {'updateGraphLastOpened': True} + updated_last_opened2 = client.query(query_last_opened)['graph']['properties']['constant']['get']['value'] + assert updated_last_opened2 > updated_last_opened1 + server.stop() - -def test_load_graphs_from_path(): - g1 = Graph() - g1.add_edge(1, "ben", "hamza") - g1.add_edge(2, "haaroon", "hamza") - g1.add_edge(3, "ben", "haaroon") - g2 = Graph() - g2.add_edge(1, "Naomi", "Shivam") - g2.add_edge(2, "Shivam", "Pedro") - g2.add_edge(3, "Pedro", "Rachel") - tmp_work_dir = tempfile.mkdtemp() - graphs = {"g1": g1, "g2": g2} - server = RaphtoryServer(tmp_work_dir, graphs=graphs).start() +def test_update_graph_last_opened_succeeds_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() client = server.get_client() - g2 = Graph() - g2.add_edge(1, "shifu", "po") - g2.add_edge(2, "oogway", "phi") - g2.add_edge(3, "phi", "po") - tmp_dir = tempfile.mkdtemp() - g2_file_path = tmp_dir + "/g2" - g2.save_to_file(g2_file_path) + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") - # Since overwrite is False by default, it will not overwrite the existing graph g2 - client.load_graphs_from_path(tmp_dir) + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) - query_g1 = """{graph(name: "g1") {nodes {list {name}}}}""" - query_g2 = """{graph(name: "g2") {nodes {list {name}}}}""" - assert client.query(query_g1) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} - } - } - assert client.query(query_g2) == { - "graph": { - "nodes": { - "list": [ - {"name": "Naomi"}, - {"name": "Shivam"}, - {"name": "Pedro"}, - {"name": "Rachel"}, - ] - } - } - } + query_last_opened = """{ graph(name: "g2", namespace: "shivam") { properties { constant { get(key: "lastOpened") { value } } } } }""" + mutate_last_opened = """mutation { updateGraphLastOpened(graphName: "g2", namespace: "shivam") }""" + assert client.query(query_last_opened) == {'graph': {'properties': {'constant': {'get': None}}}} + assert client.query(mutate_last_opened) == {'updateGraphLastOpened': True} + updated_last_opened1 = client.query(query_last_opened)['graph']['properties']['constant']['get']['value'] + time.sleep(1) + assert client.query(mutate_last_opened) == {'updateGraphLastOpened': True} + updated_last_opened2 = client.query(query_last_opened)['graph']['properties']['constant']['get']['value'] + assert updated_last_opened2 > updated_last_opened1 server.stop() -def test_load_graphs_from_path_overwrite(): - g1 = Graph() - g1.add_edge(1, "ben", "hamza") - g1.add_edge(2, "haaroon", "hamza") - g1.add_edge(3, "ben", "haaroon") - g2 = Graph() - g2.add_edge(1, "Naomi", "Shivam") - g2.add_edge(2, "Shivam", "Pedro") - g2.add_edge(3, "Pedro", "Rachel") - tmp_dir = tempfile.mkdtemp() - g2_file_path = tmp_dir + "/g2" - g2.save_to_file(g2_file_path) +def test_archive_graph_fails_if_graph_not_found(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() - tmp_work_dir = tempfile.mkdtemp() - graphs = {"g1": g1, "g2": g2} - server = RaphtoryServer(tmp_work_dir, graphs=graphs).start() + query = """mutation { archiveGraph(graphName: "g1", isArchive: 0) }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found g1" in str(e), f"Unexpected exception message: {e}" + + server.stop() + + +def test_archive_graph_fails_if_graph_not_found_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() client = server.get_client() - client.load_graphs_from_path(tmp_dir, True) - query_g1 = """{graph(name: "g1") {nodes {list {name}}}}""" - query_g2 = """{graph(name: "g2") {nodes {list {name}}}}""" - assert client.query(query_g1) == { - "graph": { - "nodes": {"list": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} - } - } - assert client.query(query_g2) == { - "graph": { - "nodes": { - "list": [ - {"name": "Naomi"}, - {"name": "Shivam"}, - {"name": "Pedro"}, - {"name": "Rachel"}, - ] - } - } - } + query = """mutation { archiveGraph(graphName: "g1", namespace: "shivam", isArchive: 0) }""" + try: + client.query(query) + except Exception as e: + assert "Graph not found shivam/g1" in str(e), f"Unexpected exception message: {e}" server.stop() +def test_archive_graph_succeeds(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + query_is_archive = """{ graph(name: "g1") { properties { constant { get(key: "isArchive") { value } } } } }""" + assert client.query(query_is_archive) == {'graph': {'properties': {'constant': {'get': None}}}} + update_archive_graph = """mutation { archiveGraph(graphName: "g1", isArchive: 0) }""" + assert client.query(update_archive_graph) == {"archiveGraph": True} + assert client.query(query_is_archive)['graph']['properties']['constant']['get']['value'] == 0 + update_archive_graph = """mutation { archiveGraph(graphName: "g1", isArchive: 1) }""" + assert client.query(update_archive_graph) == {"archiveGraph": True} + assert client.query(query_is_archive)['graph']['properties']['constant']['get']['value'] == 1 + + server.stop() + + +def test_archive_graph_succeeds_at_namespace(): + work_dir = tempfile.mkdtemp() + server = RaphtoryServer(work_dir).start() + client = server.get_client() + + g = Graph() + g.add_edge(1, "ben", "hamza") + g.add_edge(2, "haaroon", "hamza") + g.add_edge(3, "ben", "haaroon") + + os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) + g.save_to_file(os.path.join(work_dir, "g1")) + g.save_to_file(os.path.join(work_dir, "shivam", "g2")) + + query_is_archive = """{ graph(name: "g2", namespace: "shivam") { properties { constant { get(key: "isArchive") { value } } } } }""" + assert client.query(query_is_archive) == {'graph': {'properties': {'constant': {'get': None}}}} + update_archive_graph = """mutation { archiveGraph(graphName: "g2", namespace: "shivam", isArchive: 0) }""" + assert client.query(update_archive_graph) == {"archiveGraph": True} + assert client.query(query_is_archive)['graph']['properties']['constant']['get']['value'] == 0 + update_archive_graph = """mutation { archiveGraph(graphName: "g2", namespace: "shivam", isArchive: 1) }""" + assert client.query(update_archive_graph) == {"archiveGraph": True} + assert client.query(query_is_archive)['graph']['properties']['constant']['get']['value'] == 1 + + server.stop() + + def test_graph_windows_and_layers_query(): g1 = graph_loader.lotr_graph() g1.add_constant_properties({"name": "lotr"}) @@ -228,11 +2437,13 @@ def test_graph_windows_and_layers_query(): g2.add_constant_properties({"name": "layers"}) g2.add_edge(1, 1, 2, layer="layer1") g2.add_edge(1, 2, 3, layer="layer2") - + tmp_work_dir = tempfile.mkdtemp() - graphs = {"lotr": g1, "layers": g2} - server = RaphtoryServer(tmp_work_dir, graphs=graphs).start() + server = RaphtoryServer(tmp_work_dir).start() client = server.get_client() + client.send_graph(name="lotr", graph=g1) + client.send_graph(name="layers", graph=g2) + q = """ query GetEdges { graph(name: "lotr") { @@ -315,7 +2526,7 @@ def test_graph_windows_and_layers_query(): json_a = json.loads(a) json_ra = json.loads(ra) assert json_a == json_ra - + server.stop() @@ -328,8 +2539,9 @@ def test_graph_properties_query(): n.add_constant_properties({"prop5": "val4"}) tmp_work_dir = tempfile.mkdtemp() - server = RaphtoryServer(tmp_work_dir, graphs={"g": g}).start() + server = RaphtoryServer(tmp_work_dir).start() client = server.get_client() + client.send_graph(name="g", graph=g) q = """ query GetEdges { graph(name: "g") { diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index cb2ad095ae..c560e107cb 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -1,15 +1,17 @@ use crate::{ - model::algorithms::global_plugins::GlobalPlugins, - server_config::{load_config, AppConfig, CacheConfig}, + model::{ + algorithms::global_plugins::GlobalPlugins, construct_graph_name, construct_graph_path, + }, + server_config::AppConfig, }; use async_graphql::Error; use dynamic_graphql::Result; use itertools::Itertools; -use moka::sync::{Cache, CacheBuilder}; +use moka::sync::Cache; #[cfg(feature = "storage")] use raphtory::disk_graph::graph_impl::DiskGraph; use raphtory::{ - core::Prop, + core::utils::errors::GraphError, db::api::view::MaterializedGraph, prelude::{GraphViewOps, PropUnwrap, PropertyAdditionOps}, search::IndexedGraph, @@ -29,12 +31,7 @@ pub struct Data { } impl Data { - pub fn new( - work_dir: &Path, - graphs: Option>, - graph_paths: Option>, - configs: &AppConfig, - ) -> Self { + pub fn new(work_dir: &Path, configs: &AppConfig) -> Self { let cache_configs = &configs.cache; let graphs_cache_builder = Cache::builder() @@ -45,12 +42,6 @@ impl Data { let graphs_cache: Arc>> = Arc::new(graphs_cache_builder); - save_graphs_to_work_dir(work_dir, &graphs.unwrap_or_default()) - .expect("Failed to save graphs to work dir"); - - load_graphs_from_paths(work_dir, graph_paths.unwrap_or_default(), true) - .expect("Failed to save graph paths to work dir"); - Self { work_dir: work_dir.to_string_lossy().into_owned(), graphs: graphs_cache, @@ -58,18 +49,22 @@ impl Data { } } - pub fn get_graph(&self, name: &str) -> Result>> { + pub fn get_graph( + &self, + name: &str, + namespace: &Option, + ) -> Result> { + let path = construct_graph_path(&self.work_dir, name, namespace)?; let name = name.to_string(); - let path = Path::new(&self.work_dir).join(name.as_str()); if !path.exists() { - Ok(None) + return Err(GraphError::GraphNotFound(construct_graph_name(&name, namespace)).into()); } else { match self.graphs.get(&name) { - Some(graph) => Ok(Some(graph.clone())), - None => match get_graph_from_path(Path::new(&self.work_dir), &path)? { - Some((_, graph)) => Ok(Some(self.graphs.get_with(name, || graph))), - None => Ok(None), - }, + Some(graph) => Ok(graph.clone()), + None => { + let (_, graph) = get_graph_from_path(&path)?; + Ok(self.graphs.get_with(name, || graph)) + } } } } @@ -80,6 +75,10 @@ impl Data { .collect_vec()) } + pub fn get_graph_names_namespaces(&self) -> Result<(Vec, Vec>)> { + get_graph_names_namespaces_from_work_dir(Path::new(&self.work_dir)) + } + #[allow(dead_code)] // TODO: use this for loading both regular and vectorised graphs #[allow(dead_code)] @@ -128,16 +127,20 @@ fn copy_dir_recursive(source_dir: &Path, target_dir: &Path) -> Result<()> { fn load_disk_graph_from_path( work_dir: &Path, path: &Path, + namespace: &Option, overwrite: bool, ) -> Result> { let (name, graph) = load_disk_graph(path)?; let graph_dir = &graph.into_disk_graph().unwrap().graph_dir; - let target_dir = &Path::new(work_dir).join(name.as_str()); + let target_dir = + &construct_graph_path(&work_dir.display().to_string(), name.as_str(), namespace)?; if target_dir.exists() { if overwrite { fs::remove_dir_all(target_dir)?; copy_dir_recursive(graph_dir, target_dir)?; println!("Disk Graph loaded = {}", path.display()); + } else { + return Err(GraphError::GraphNameAlreadyExists { name }.into()); } } else { copy_dir_recursive(graph_dir, target_dir)?; @@ -150,49 +153,62 @@ fn load_disk_graph_from_path( fn load_disk_graph_from_path( work_dir: &Path, path: &Path, + namespace: &Option, overwrite: bool, ) -> Result> { Ok(None) } -// If overwrite is false, graphs with names that already exist in the work directory will be silently ignored! -fn load_graph_from_path(work_dir: &Path, path: &Path, overwrite: bool) -> Result> { +pub fn load_graph_from_path( + work_dir: &Path, + path: &Path, + namespace: &Option, + overwrite: bool, +) -> Result { if !path.exists() { - return Ok(None); + return Err(GraphError::InvalidPath(path.to_path_buf()).into()); } println!("Loading graph from {}", path.display()); if path.is_dir() { if is_disk_graph_dir(path) { - load_disk_graph_from_path(work_dir, path, overwrite) + load_disk_graph_from_path(work_dir, path, namespace, overwrite)? + .ok_or(GraphError::DiskGraphNotFound.into()) } else { - Ok(None) + Err(GraphError::InvalidPath(path.to_path_buf()).into()) } } else { - let (name, graph) = load_bincode_graph(work_dir, path)?; - let path = Path::new(work_dir).join(name.as_str()); + let (name, graph) = load_bincode_graph(path)?; + let path = construct_graph_path(&work_dir.display().to_string(), name.as_str(), namespace)?; if path.exists() { if overwrite { fs::remove_file(&path)?; graph.save_to_file(&path)?; println!("Graph loaded = {}", path.display()); + } else { + return Err(GraphError::GraphNameAlreadyExists { name }.into()); } } else { graph.save_to_file(&path)?; println!("Graph loaded = {}", path.display()); } - Ok(Some(name)) + Ok(name) } } // The default behaviour is to just override the existing graphs. // Always returns list of newly loaded graphs and not all the graphs available in the work dir. -pub fn load_graphs_from_path(work_dir: &Path, path: &Path, overwrite: bool) -> Result> { +pub fn load_graphs_from_path( + work_dir: &Path, + path: &Path, + namespace: &Option, + overwrite: bool, +) -> Result> { println!("loading graphs from {}", path.display()); let entries = fs::read_dir(path).unwrap(); entries - .filter_map(|entry| { + .map(|entry| { let path = entry.unwrap().path(); - load_graph_from_path(work_dir, &path, overwrite).transpose() + load_graph_from_path(work_dir, &path, namespace, overwrite) }) .collect() } @@ -200,11 +216,12 @@ pub fn load_graphs_from_path(work_dir: &Path, path: &Path, overwrite: bool) -> R fn load_graphs_from_paths( work_dir: &Path, paths: Vec, + namespace: &Option, overwrite: bool, ) -> Result> { paths .iter() - .filter_map(|path| load_graph_from_path(work_dir, path, overwrite).transpose()) + .map(|path| load_graph_from_path(work_dir, path, namespace, overwrite)) .collect() } @@ -224,49 +241,90 @@ fn get_disk_graph_from_path( Ok(None) } -fn get_graph_from_path( - work_dir: &Path, - path: &Path, -) -> Result)>, Error> { +fn get_graph_from_path(path: &Path) -> Result<(String, IndexedGraph), Error> { if !path.exists() { - return Ok(None); + return Err(GraphError::InvalidPath(path.to_path_buf()).into()); } if path.is_dir() { if is_disk_graph_dir(path) { - get_disk_graph_from_path(path) + get_disk_graph_from_path(path)?.ok_or(GraphError::DiskGraphNotFound.into()) } else { - Ok(None) + return Err(GraphError::InvalidPath(path.to_path_buf()).into()); } } else { - let (name, graph) = load_bincode_graph(work_dir, path)?; + let (name, graph) = load_bincode_graph(path)?; println!("Graph loaded = {}", path.display()); - Ok(Some((name, IndexedGraph::from_graph(&graph.into())?))) + Ok((name, IndexedGraph::from_graph(&graph.into())?)) } } +// We are loading all the graphs in the work dir for vectorized APIs pub(crate) fn get_graphs_from_work_dir( work_dir: &Path, ) -> Result>> { - fn get_graph_paths(work_dir: &Path) -> Vec { - let mut paths = Vec::new(); - if let Ok(entries) = fs::read_dir(work_dir) { + let mut graphs = HashMap::new(); + for path in get_graph_paths(work_dir) { + let (name, graph) = get_graph_from_path(&path)?; + graphs.insert(name, graph); + } + Ok(graphs) +} + +pub(crate) fn get_graph_names_namespaces_from_work_dir( + work_dir: &Path, +) -> Result<(Vec, Vec>)> { + let mut names = vec![]; + let mut namespaces = vec![]; + for path in get_graph_paths(work_dir) { + let (name, _) = get_graph_from_path(&path)?; + names.push(name); + namespaces.push(get_namespace_from_path(work_dir, path)); + } + + Ok((names, namespaces)) +} + +fn get_namespace_from_path(work_dir: &Path, path: PathBuf) -> Option { + let relative_path = match path.strip_prefix(work_dir) { + Ok(relative_path) => relative_path, + Err(_) => return None, // Skip paths that cannot be stripped + }; + + let parent_path = relative_path.parent().unwrap_or(Path::new("")); + if let Some(parent_str) = parent_path.to_str() { + if parent_str.is_empty() { + None + } else { + Some(parent_str.to_string()) + } + } else { + None + } +} + +fn get_graph_paths(work_dir: &Path) -> Vec { + fn traverse_directory(dir: &Path, paths: &mut Vec) { + if let Ok(entries) = fs::read_dir(dir) { for entry in entries { if let Ok(entry) = entry { - paths.push(entry.path()); + let path = entry.path(); + if path.is_dir() { + if is_disk_graph_dir(&path) { + paths.push(path); + } else { + traverse_directory(&path, paths); + } + } else if path.is_file() { + paths.push(path); + } } } } - paths } - let mut graphs = HashMap::new(); - for path in get_graph_paths(work_dir) { - if let Some((name, graph)) = get_graph_from_path(work_dir, &path)? { - graphs.insert(name, graph); - } - } - - Ok(graphs) + let mut paths = Vec::new(); + traverse_directory(work_dir, &mut paths); + paths } fn is_disk_graph_dir(path: &Path) -> bool { @@ -283,34 +341,28 @@ fn is_disk_graph_dir(path: &Path) -> bool { has_disk_graph_files } -fn get_graph_name(path: &Path, graph: &MaterializedGraph) -> String { - graph - .properties() - .get("name") - .into_str() - .map(|v| v.to_string()) - .unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap().to_owned()) +fn get_graph_name(path: &Path) -> Result { + path.file_name() + .and_then(|os_str| os_str.to_str()) + .map(|str_slice| str_slice.to_string()) + .ok_or("No file name found in the path or invalid UTF-8") } -fn save_graphs_to_work_dir( +pub(crate) fn save_graphs_to_work_dir( work_dir: &Path, + namespace: &Option, graphs: &HashMap, ) -> Result<()> { for (name, graph) in graphs { - let path = work_dir.join(&name); + let path = construct_graph_path(&work_dir.display().to_string(), name, &namespace)?; graph.save_to_path(&path)?; } Ok(()) } -fn load_bincode_graph(work_dir: &Path, path: &Path) -> Result<(String, MaterializedGraph)> { +fn load_bincode_graph(path: &Path) -> Result<(String, MaterializedGraph)> { let graph = MaterializedGraph::load_from_file(path, false)?; - let name = get_graph_name(path, &graph); - let path = Path::new(work_dir).join(name.as_str()); - graph.update_constant_properties([( - "path".to_string(), - Prop::str(path.display().to_string().clone()), - )])?; + let name = get_graph_name(path)?; Ok((name, graph)) } @@ -318,8 +370,7 @@ fn load_bincode_graph(work_dir: &Path, path: &Path) -> Result<(String, Materiali fn load_disk_graph(path: &Path) -> Result<(String, MaterializedGraph)> { let disk_graph = DiskGraph::load_from_dir(path)?; let graph: MaterializedGraph = disk_graph.into(); - let graph_name = get_graph_name(path, &graph); - + let graph_name = get_graph_name(path)?; Ok((graph_name, graph)) } @@ -333,10 +384,11 @@ fn load_disk_graph(path: &Path) -> Result<(String, MaterializedGraph)> { mod data_tests { use crate::{ data::{ - get_graph_from_path, get_graphs_from_work_dir, load_graph_from_path, - load_graphs_from_path, load_graphs_from_paths, save_graphs_to_work_dir, Data, + get_graph_from_path, get_graph_paths, get_graphs_from_work_dir, + get_namespace_from_path, load_graph_from_path, load_graphs_from_path, + load_graphs_from_paths, save_graphs_to_work_dir, Data, }, - server_config::{AppConfig, CacheConfig, LoggingConfig}, + server_config::AppConfigBuilder, }; use itertools::Itertools; #[cfg(feature = "storage")] @@ -345,7 +397,7 @@ mod data_tests { db::api::view::MaterializedGraph, prelude::{AdditionOps, Graph, GraphViewOps, PropertyAdditionOps}, }; - use std::{collections::HashMap, fs, io, path::Path, thread, time::Duration}; + use std::{collections::HashMap, fs, fs::File, io, path::Path, thread, time::Duration}; fn list_top_level_files_and_dirs(path: &Path) -> io::Result> { let mut entries_vec = Vec::new(); @@ -397,33 +449,67 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_g"); graph.save_to_file(&graph_path).unwrap(); - let res = load_graph_from_path(tmp_work_dir.path(), &graph_path, true) - .unwrap() - .unwrap(); + let res = load_graph_from_path(tmp_work_dir.path(), &graph_path, &None, true).unwrap(); assert_eq!(res, "test_g"); let graph = Graph::load_from_file(tmp_work_dir.path().join("test_g"), false).unwrap(); assert_eq!(graph.count_edges(), 2); - // Dir path doesn't exists - let res = load_graph_from_path( - tmp_work_dir.path(), - &tmp_graph_dir.path().join("test_dg1"), - true, - ) - .unwrap(); - assert!(res.is_none()); + // Test directory path doesn't exist + let result = std::panic::catch_unwind(|| { + load_graph_from_path( + tmp_work_dir.path(), + &tmp_graph_dir.path().join("test_dg1"), + &None, + true, + ) + .unwrap(); + }); + + // Assert that it panicked with the expected message + assert!(result.is_err()); + if let Err(err) = result { + let panic_message = err + .downcast_ref::() + .expect("Expected a String panic message"); + assert!( + panic_message.contains("Invalid path:"), + "Unexpected panic message: {}", + panic_message + ); + assert!( + panic_message.contains("test_dg1"), + "Unexpected panic message: {}", + panic_message + ); + } // Dir path exists but is not a disk graph path - let tmp_graph_dir = tempfile::tempdir().unwrap(); - let res = load_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path(), true).unwrap(); - assert!(res.is_none()); + let result = std::panic::catch_unwind(|| { + load_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path(), &None, true).unwrap(); + }); + + // Assert that it panicked with the expected message + assert!(result.is_err()); + if let Err(err) = result { + let panic_message = err + .downcast_ref::() + .expect("Expected a String panic message"); + assert!( + panic_message.contains("Invalid path:"), + "Unexpected panic message: {}", + panic_message + ); + } // Dir path exists and is a disk graph path but storage feature is disabled let graph_path = tmp_graph_dir.path().join("test_dg"); create_ipc_files_in_dir(&graph_path).unwrap(); - let res = load_graph_from_path(tmp_work_dir.path(), &graph_path, true).unwrap(); - assert!(res.is_none()); + let res = load_graph_from_path(tmp_work_dir.path(), &graph_path, &None, true); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Disk graph not found")); + } } #[test] @@ -443,9 +529,7 @@ mod data_tests { let graph_dir = tmp_graph_dir.path().join("test_dg"); let _ = DiskGraph::from_graph(&graph, &graph_dir).unwrap(); - let res = load_graph_from_path(tmp_work_dir.path(), &graph_dir, true) - .unwrap() - .unwrap(); + let res = load_graph_from_path(tmp_work_dir.path(), &graph_dir, &None, true).unwrap(); assert_eq!(res, "test_dg"); let graph = DiskGraph::load_from_dir(tmp_work_dir.path().join("test_dg")).unwrap(); @@ -480,7 +564,8 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_g2"); graph.save_to_file(&graph_path).unwrap(); - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), true).unwrap(); + let res = + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_g2", "test_g1"]); let graph = Graph::load_from_file(tmp_work_dir.path().join("test_g1"), false).unwrap(); @@ -506,14 +591,27 @@ mod data_tests { graph.save_to_file(&graph_path).unwrap(); // Test overwrite false - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), false).unwrap(); - assert_eq!(res, vec!["test_g2", "test_g1"]); + let result = std::panic::catch_unwind(|| { + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, false).unwrap(); + }); + assert!(result.is_err()); + if let Err(err) = result { + let panic_message = err + .downcast_ref::() + .expect("Expected a String panic message"); + assert!( + panic_message.contains("Graph already exists by name = test_g2"), + "Unexpected panic message: {}", + panic_message + ); + } let graph = Graph::load_from_file(tmp_work_dir.path().join("test_g2"), false).unwrap(); assert_eq!(graph.count_edges(), 3); // Test overwrite true - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), true).unwrap(); + let res = + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_g2", "test_g1"]); let graph = Graph::load_from_file(tmp_work_dir.path().join("test_g2"), false).unwrap(); @@ -549,7 +647,8 @@ mod data_tests { let graph_dir = tmp_graph_dir.path().join("test_dg2"); let _ = DiskGraph::from_graph(&graph, &graph_dir).unwrap(); - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), true).unwrap(); + let res = + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_dg2", "test_dg1"]); let graph = DiskGraph::load_from_dir(tmp_work_dir.path().join("test_dg1")).unwrap(); @@ -589,7 +688,7 @@ mod data_tests { assert_eq!(graph.count_edges(), 4); // Test overwrite false - let res = load_graphs_from_path(tmp_work_dir, tmp_graph_dir.path(), false).unwrap(); + let res = load_graphs_from_path(tmp_work_dir, tmp_graph_dir.path(), &None, false).unwrap(); assert_eq!(res, vec!["test_dg2"]); let graphs_paths = list_top_level_files_and_dirs(tmp_work_dir).unwrap(); @@ -599,7 +698,7 @@ mod data_tests { assert_eq!(graph.count_edges(), 3); // Test overwrite true - let res = load_graphs_from_path(tmp_work_dir, tmp_graph_dir.path(), true).unwrap(); + let res = load_graphs_from_path(tmp_work_dir, tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_dg2"]); let graphs_paths = list_top_level_files_and_dirs(tmp_work_dir).unwrap(); @@ -637,8 +736,13 @@ mod data_tests { let graph_path2 = tmp_graph_dir.path().join("test_g2"); graph.save_to_file(&graph_path2).unwrap(); - let res = load_graphs_from_paths(tmp_work_dir.path(), vec![graph_path1, graph_path2], true) - .unwrap(); + let res = load_graphs_from_paths( + tmp_work_dir.path(), + vec![graph_path1, graph_path2], + &None, + true, + ) + .unwrap(); assert_eq!(res, vec!["test_g1", "test_g2"]); let graph = Graph::load_from_file(tmp_work_dir.path().join("test_g1"), false).unwrap(); @@ -677,8 +781,13 @@ mod data_tests { let graph_path2 = tmp_graph_dir.path().join("test_dg2"); let _ = DiskGraph::from_graph(&graph, &graph_path2).unwrap(); - let res = load_graphs_from_paths(tmp_work_dir.path(), vec![graph_path1, graph_path2], true) - .unwrap(); + let res = load_graphs_from_paths( + tmp_work_dir.path(), + vec![graph_path1, graph_path2], + &None, + true, + ) + .unwrap(); assert_eq!(res, vec!["test_dg1", "test_dg2"]); let graph = DiskGraph::load_from_dir(tmp_work_dir.path().join("test_dg1")).unwrap(); @@ -704,32 +813,39 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_g1"); graph.save_to_file(&graph_path).unwrap(); - let res = get_graph_from_path(tmp_work_dir.path(), &graph_path).unwrap(); - - assert!(res.is_some()); - let res = res.unwrap(); + let res = get_graph_from_path(&graph_path).unwrap(); assert_eq!(res.0, "test_g1"); assert_eq!(res.1.graph.into_events().unwrap().count_edges(), 2); - let res = get_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path().join("test_g2")) - .unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&tmp_graph_dir.path().join("test_g2")); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Invalid path")); + } // Dir path doesn't exists - let res = get_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path().join("test_dg1")) - .unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&tmp_graph_dir.path().join("test_dg1")); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Invalid path")); + } // Dir path exists but is not a disk graph path let tmp_graph_dir = tempfile::tempdir().unwrap(); - let res = get_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path()).unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&tmp_graph_dir.path()); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Invalid path")); + } // Dir path exists and is a disk graph path but storage feature is disabled let graph_path = tmp_graph_dir.path().join("test_dg"); create_ipc_files_in_dir(&graph_path).unwrap(); - let res = get_graph_from_path(tmp_work_dir.path(), &graph_path).unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&graph_path); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Disk graph not found")); + } } #[test] @@ -748,22 +864,24 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_dg"); let _ = DiskGraph::from_graph(&graph, &graph_path).unwrap(); - let res = get_graph_from_path(tmp_work_dir.path(), &graph_path).unwrap(); - - assert!(res.is_some()); - let res = res.unwrap(); + let res = get_graph_from_path(&graph_path).unwrap(); assert_eq!(res.0, "test_dg"); assert_eq!(res.1.graph.into_disk_graph().unwrap().count_edges(), 2); // Dir path doesn't exists - let res = get_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path().join("test_dg1")) - .unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&tmp_graph_dir.path().join("test_dg1")); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Invalid path")); + } // Dir path exists but is not a disk graph path let tmp_graph_dir = tempfile::tempdir().unwrap(); - let res = get_graph_from_path(tmp_work_dir.path(), &tmp_graph_dir.path()).unwrap(); - assert!(res.is_none()); + let res = get_graph_from_path(&tmp_graph_dir.path()); + assert!(res.is_err()); + if let Err(err) = res { + assert!(err.message.contains("Invalid path")); + } } #[test] @@ -794,7 +912,8 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_g2"); graph.save_to_file(&graph_path).unwrap(); - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), true).unwrap(); + let res = + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_g2", "test_g1"]); let res = get_graphs_from_work_dir(tmp_work_dir.path()).unwrap(); @@ -833,7 +952,8 @@ mod data_tests { let graph_path = tmp_graph_dir.path().join("test_dg2"); let _ = DiskGraph::from_graph(&graph, &graph_path).unwrap(); - let res = load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), true).unwrap(); + let res = + load_graphs_from_path(tmp_work_dir.path(), tmp_graph_dir.path(), &None, true).unwrap(); assert_eq!(res, vec!["test_dg2", "test_dg1"]); let res = get_graphs_from_work_dir(tmp_work_dir.path()).unwrap(); @@ -867,7 +987,7 @@ mod data_tests { ("test_dg".to_string(), graph2), ]); - save_graphs_to_work_dir(tmp_graph_dir.path(), &graphs).unwrap(); + save_graphs_to_work_dir(tmp_graph_dir.path(), &None, &graphs).unwrap(); let graphs = list_top_level_files_and_dirs(tmp_graph_dir.path()).unwrap(); assert_eq!(graphs, vec!["test_g", "test_dg"]); @@ -896,30 +1016,22 @@ mod data_tests { let graph_path3 = tmp_graph_dir.path().join("test_g2"); graph.save_to_file(&graph_path3).unwrap(); - let configs = AppConfig { - logging: LoggingConfig::default(), - cache: CacheConfig { - capacity: 1, - tti_seconds: 2, - }, - }; + let configs = AppConfigBuilder::new() + .with_cache_capacity(1) + .with_cache_tti_seconds(2) + .build(); - let data = Data::new( - tmp_work_dir.path(), - None, - Some(vec![graph_path1, graph_path2, graph_path3]), - configs, - ); + let data = Data::new(tmp_work_dir.path(), &configs); assert!(!data.graphs.contains_key("test_dg")); assert!(!data.graphs.contains_key("test_g")); // Test size based eviction - let _ = data.get_graph("test_dg"); + let _ = data.get_graph("test_dg", &None); assert!(data.graphs.contains_key("test_dg")); assert!(!data.graphs.contains_key("test_g")); - let _ = data.get_graph("test_g"); + let _ = data.get_graph("test_g", &None); // data.graphs.iter().for_each(|(k, _)| println!("{}", k)); // assert!(!data.graphs.contains_key("test_dg")); assert!(data.graphs.contains_key("test_g")); @@ -928,4 +1040,61 @@ mod data_tests { assert!(!data.graphs.contains_key("test_dg")); assert!(!data.graphs.contains_key("test_g")); } + + #[test] + fn test_get_graph_paths() { + let temp_dir = tempfile::tempdir().unwrap(); + let work_dir = temp_dir.path(); + let g0_path = work_dir.join("g0"); + let g1_path = work_dir.join("g1"); + let g2_path = work_dir + .join("shivam") + .join("investigations") + .join("2024-12-22") + .join("g2"); + let g3_path = work_dir.join("shivam").join("investigations").join("g3"); // Graph + let g4_path = work_dir.join("shivam").join("investigations").join("g4"); // Disk graph dir + let g5_path = work_dir.join("shivam").join("investigations").join("g5"); // Empty dir + + fs::create_dir_all( + &work_dir + .join("shivam") + .join("investigations") + .join("2024-12-22"), + ) + .unwrap(); + fs::create_dir_all(&g4_path).unwrap(); + create_ipc_files_in_dir(&g4_path).unwrap(); + fs::create_dir_all(&g5_path).unwrap(); + + File::create(&g0_path).unwrap(); + File::create(&g1_path).unwrap(); + File::create(&g2_path).unwrap(); + File::create(&g3_path).unwrap(); + + let paths = get_graph_paths(work_dir); + + assert_eq!(paths.len(), 5); + assert!(paths.contains(&g0_path)); + assert!(paths.contains(&g1_path)); + assert!(paths.contains(&g2_path)); + assert!(paths.contains(&g3_path)); + assert!(paths.contains(&g4_path)); + assert!(!paths.contains(&g5_path)); // Empty dir are ignored + + assert_eq!(get_namespace_from_path(work_dir, g0_path), None); + assert_eq!(get_namespace_from_path(work_dir, g1_path), None); + assert_eq!( + get_namespace_from_path(work_dir, g2_path), + Some("shivam/investigations/2024-12-22".to_string()) + ); + assert_eq!( + get_namespace_from_path(work_dir, g3_path), + Some("shivam/investigations".to_string()) + ); + assert_eq!( + get_namespace_from_path(work_dir, g4_path), + Some("shivam/investigations".to_string()) + ); + } } diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index ffe4c8b3d2..5693d43bac 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -1,6 +1,4 @@ pub use crate::server::RaphtoryServer; -use base64::{prelude::BASE64_URL_SAFE_NO_PAD, DecodeError, Engine}; -use raphtory::{core::utils::errors::GraphError, db::api::view::MaterializedGraph}; pub mod azure_auth; mod data; @@ -9,49 +7,29 @@ mod observability; mod routes; pub mod server; pub mod server_config; - -#[derive(thiserror::Error, Debug)] -pub enum UrlDecodeError { - #[error("Bincode operation failed")] - BincodeError { - #[from] - source: Box, - }, - #[error("Base64 decoding failed")] - DecodeError { - #[from] - source: DecodeError, - }, -} - -pub fn url_encode_graph>(graph: G) -> Result { - let g: MaterializedGraph = graph.into(); - Ok(BASE64_URL_SAFE_NO_PAD.encode(bincode::serialize(&g)?)) -} - -pub fn url_decode_graph>(graph: T) -> Result { - Ok(bincode::deserialize( - &BASE64_URL_SAFE_NO_PAD.decode(graph)?, - )?) -} +pub mod url_encode; #[cfg(test)] mod graphql_test { - use super::*; - use crate::{data::Data, model::App, server_config::AppConfig}; + use crate::{ + data::{save_graphs_to_work_dir, Data}, + model::App, + server_config::AppConfigBuilder, + url_encode::{url_decode_graph, url_encode_graph}, + }; use async_graphql::UploadValue; use dynamic_graphql::{Request, Variables}; #[cfg(feature = "storage")] use raphtory::disk_graph::graph_impl::DiskGraph; use raphtory::{ - db::{api::view::IntoDynamic, graph::views::deletion_graph::PersistentGraph}, + db::{ + api::view::{IntoDynamic, MaterializedGraph}, + graph::views::deletion_graph::PersistentGraph, + }, prelude::*, }; use serde_json::json; - use std::{ - collections::{HashMap, HashSet}, - path::Path, - }; + use std::collections::{HashMap, HashSet}; use tempfile::{tempdir, TempDir}; #[tokio::test] @@ -79,7 +57,10 @@ mod graphql_test { let graphs = HashMap::from([("lotr".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); + let schema = App::create_schema().data(data).finish().unwrap(); let query = r#" @@ -120,7 +101,9 @@ mod graphql_test { let graph: MaterializedGraph = graph.into(); let graphs = HashMap::from([("lotr".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); @@ -170,7 +153,9 @@ mod graphql_test { let graphs = HashMap::from([("graph".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let prop_has_key_filter = r#" { @@ -233,7 +218,9 @@ mod graphql_test { let graph: MaterializedGraph = g.into(); let graphs = HashMap::from([("graph".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let prop_has_key_filter = r#" @@ -423,7 +410,9 @@ mod graphql_test { let g = g.into(); let graphs = HashMap::from([("graph".to_string(), g)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let prop_has_key_filter = r#" @@ -670,7 +659,9 @@ mod graphql_test { let graph = graph.into(); let graphs = HashMap::from([("graph".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let prop_has_key_filter = r#" { @@ -724,12 +715,12 @@ mod graphql_test { g2.add_node(0, 2, [("name", "2")], None).unwrap(); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), None, None, &AppConfig::default()); + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let list_graphs = r#"{ graphs { - names + name } }"#; @@ -756,21 +747,6 @@ mod graphql_test { true ); - let save_graph = |parent_name: &str, new_graph_name: &str, nodes: &str| { - format!( - r#"mutation {{ - saveGraph( - parentGraphName: "{parent_name}", - graphName: "{parent_name}", - newGraphName: "{new_graph_name}", - props: "{{}}", - isArchive: 0, - graphNodes: {nodes}, - ) - }}"# - ) - }; - // only g0 which is empty let req = Request::new(load_all); let res = schema.execute(req).await; @@ -780,7 +756,7 @@ mod graphql_test { let req = Request::new(list_graphs); let res = schema.execute(req).await; let res_json = res.data.into_json().unwrap(); - assert_eq!(res_json, json!({"graphs": {"names": ["g0"]}})); + assert_eq!(res_json, json!({"graphs": {"name": ["g0"]}})); let req = Request::new(list_nodes("g0")); let res = schema.execute(req).await; @@ -813,28 +789,6 @@ mod graphql_test { res_json, json!({"graph": {"nodes": {"list": [{"id": "1"}]}}}) ); - - // Test save graph - let req = Request::new(save_graph("g0", "g3", r#""{ \"2\": {} }""#)); - let res = schema.execute(req).await; - assert!(res.errors.is_empty()); - let req = Request::new(list_nodes("g3")); - let res = schema.execute(req).await; - let res_json = res.data.into_json().unwrap(); - assert_eq!( - res_json, - json!({"graph": {"nodes": {"list": [{"id": "2"}]}}}) - ); - - // Test exception case when saving with a name against which there already exists a graph - let req = Request::new(save_graph("g0", "g3", r#""{ \"2\": {} }""#)); - let res = schema.execute(req).await; - let data = res.errors; - let error_message = &data[0].message; - let expected_error_message = "Graph already exists by name = g3"; - assert_eq!(error_message, expected_error_message); - - // TODO: Add a test that we can update the isArchive on the graph while saving it by the same name as well } #[tokio::test] @@ -852,16 +806,16 @@ mod graphql_test { }; let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), None, None, &AppConfig::default()); + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let query = r##" - mutation($file: Upload!) { - uploadGraph(name: "test", graph: $file) + mutation($file: Upload!, $overwrite: Boolean!) { + uploadGraph(name: "test", graph: $file, overwrite: $overwrite) } "##; - let variables = json!({ "file": null }); + let variables = json!({ "file": null, "overwrite": false }); let mut req = Request::new(query).variables(Variables::from_json(variables)); req.set_upload("variables.file", upload_val); let res = schema.execute(req).await; @@ -893,6 +847,7 @@ mod graphql_test { } #[tokio::test] + #[ignore] async fn test_graph_send_receive_base64() { let g = PersistentGraph::new(); g.add_node(0, 1, NO_PROPS, None).unwrap(); @@ -900,16 +855,17 @@ mod graphql_test { let graph_str = url_encode_graph(g.clone()).unwrap(); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), None, None, &AppConfig::default()); + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let query = r#" - mutation($graph: String!) { - sendGraph(name: "test", graph: $graph) + mutation($graph: String!, $overwrite: Boolean!) { + sendGraph(name: "test", graph: $graph, overwrite: $overwrite) } "#; - let req = - Request::new(query).variables(Variables::from_json(json!({ "graph": graph_str }))); + let req = Request::new(query).variables(Variables::from_json( + json!({ "graph": graph_str, "overwrite": false }), + )); let res = schema.execute(req).await; assert_eq!(res.errors.len(), 0); @@ -973,7 +929,9 @@ mod graphql_test { let graph = graph.into(); let graphs = HashMap::from([("graph".to_string(), graph)]); let tmp_dir = tempdir().unwrap(); - let data = Data::new(tmp_dir.path(), Some(graphs), None, &AppConfig::default()); + save_graphs_to_work_dir(tmp_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let req = r#" @@ -1094,12 +1052,9 @@ mod graphql_test { let graph = disk_graph.into(); let graphs = HashMap::from([("graph".to_string(), graph)]); let tmp_work_dir = tempdir().unwrap(); - let data = Data::new( - tmp_work_dir.path(), - Some(graphs), - None, - &AppConfig::default(), - ); + save_graphs_to_work_dir(tmp_work_dir.path(), &None, &graphs).unwrap(); + + let data = Data::new(tmp_work_dir.path(), &AppConfigBuilder::new().build()); let schema = App::create_schema().data(data).finish().unwrap(); let req = r#" diff --git a/raphtory-graphql/src/main.rs b/raphtory-graphql/src/main.rs index d905a79ed3..ba03f1445e 100644 --- a/raphtory-graphql/src/main.rs +++ b/raphtory-graphql/src/main.rs @@ -1,33 +1,24 @@ -use crate::server::RaphtoryServer; -use std::{env, path::Path}; - -mod azure_auth; -mod data; -mod model; -mod observability; -mod routes; -mod server; -mod server_config; - -extern crate base64_compat as base64_compat; +use raphtory_graphql::RaphtoryServer; +use std::{env, path::PathBuf}; +use tokio::io::Result as IoResult; #[tokio::main] -async fn main() { +async fn main() -> IoResult<()> { let work_dir = env::var("GRAPH_DIRECTORY").unwrap_or("/tmp/graphs".to_string()); - let work_dir = Path::new(&work_dir); + let work_dir = PathBuf::from(&work_dir); let args: Vec = env::args().collect(); let use_auth = args.contains(&"--server".to_string()); if use_auth { - RaphtoryServer::new(&work_dir, None, None, None, None, None) - .run_with_auth(None, false) - .await - .unwrap(); + RaphtoryServer::new(work_dir, None, None)? + .run_with_auth(false) + .await?; } else { - RaphtoryServer::new(&work_dir, None, None, None, None, None) - .run(None, false) - .await - .unwrap(); + RaphtoryServer::new(work_dir, None, None)? + .run(false) + .await?; } + + Ok(()) } diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index 60b89aadb9..30616497d7 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -22,13 +22,15 @@ use std::{collections::HashSet, convert::Into}; #[derive(ResolvedObject)] pub(crate) struct GqlGraph { name: String, + namespace: Option, graph: IndexedGraph, } impl GqlGraph { - pub fn new(name: String, graph: G) -> Self { + pub fn new(name: String, namespace: Option, graph: G) -> Self { Self { name, + namespace, graph: graph.into_dynamic_indexed(), } } @@ -45,80 +47,141 @@ impl GqlGraph { } async fn default_layer(&self) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.default_layer()) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.default_layer(), + ) } async fn layers(&self, names: Vec) -> GqlGraph { let name = self.name.clone(); - GqlGraph::new(name, self.graph.valid_layers(names)) + GqlGraph::new(name, self.namespace.clone(), self.graph.valid_layers(names)) } async fn exclude_layers(&self, names: Vec) -> GqlGraph { let name = self.name.clone(); - GqlGraph::new(name, self.graph.exclude_valid_layers(names)) + GqlGraph::new( + name, + self.namespace.clone(), + self.graph.exclude_valid_layers(names), + ) } async fn layer(&self, name: String) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.valid_layers(name)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.valid_layers(name), + ) } async fn exclude_layer(&self, name: String) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.exclude_valid_layers(name)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.exclude_valid_layers(name), + ) } async fn subgraph(&self, nodes: Vec) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.subgraph(nodes)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.subgraph(nodes), + ) } async fn subgraph_id(&self, nodes: Vec) -> GqlGraph { let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - GqlGraph::new(self.name.clone(), self.graph.subgraph(nodes)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.subgraph(nodes), + ) } async fn subgraph_node_types(&self, node_types: Vec) -> GqlGraph { GqlGraph::new( self.name.clone(), + self.namespace.clone(), self.graph.subgraph_node_types(node_types), ) } async fn exclude_nodes(&self, nodes: Vec) -> GqlGraph { let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - GqlGraph::new(self.name.clone(), self.graph.exclude_nodes(nodes)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.exclude_nodes(nodes), + ) } async fn exclude_nodes_id(&self, nodes: Vec) -> GqlGraph { let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - GqlGraph::new(self.name.clone(), self.graph.exclude_nodes(nodes)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.exclude_nodes(nodes), + ) } /// Return a graph containing only the activity between `start` and `end` measured as milliseconds from epoch async fn window(&self, start: i64, end: i64) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.window(start, end)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.window(start, end), + ) } async fn at(&self, time: i64) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.at(time)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.at(time), + ) } async fn before(&self, time: i64) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.before(time)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.before(time), + ) } async fn after(&self, time: i64) -> GqlGraph { - GqlGraph::new(self.name.clone(), self.graph.after(time)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.after(time), + ) } async fn shrink_window(&self, start: i64, end: i64) -> Self { - GqlGraph::new(self.name.clone(), self.graph.shrink_window(start, end)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.shrink_window(start, end), + ) } async fn shrink_start(&self, start: i64) -> Self { - GqlGraph::new(self.name.clone(), self.graph.shrink_start(start)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.shrink_start(start), + ) } async fn shrink_end(&self, end: i64) -> Self { - GqlGraph::new(self.name.clone(), self.graph.shrink_end(end)) + GqlGraph::new( + self.name.clone(), + self.namespace.clone(), + self.graph.shrink_end(end), + ) } //////////////////////// @@ -227,6 +290,7 @@ impl GqlGraph { async fn node(&self, name: String) -> Option { self.graph.node(name).map(|v| v.into()) } + async fn node_id(&self, id: u64) -> Option { self.graph.node(id).map(|v| v.into()) } @@ -243,6 +307,7 @@ impl GqlGraph { .map(|vv| vv.into()) .collect() } + async fn fuzzy_search_nodes( &self, query: String, @@ -312,6 +377,13 @@ impl GqlGraph { self.name.clone() } + async fn path(&self) -> String { + match self.namespace.clone() { + Some(ns) => format!("{}/{}", ns, self.name.clone()), + None => self.name.clone(), + } + } + async fn schema(&self) -> GraphSchema { GraphSchema::new(self.graph.graph()) } diff --git a/raphtory-graphql/src/model/graph/graphs.rs b/raphtory-graphql/src/model/graph/graphs.rs index 37b37942ea..6a050a5003 100644 --- a/raphtory-graphql/src/model/graph/graphs.rs +++ b/raphtory-graphql/src/model/graph/graphs.rs @@ -1,48 +1,34 @@ -use crate::model::graph::property::GqlProperties; +use async_graphql::parser::Error; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; -use itertools::Itertools; -use raphtory::{ - db::api::{properties::dyn_props::DynProperties, view::DynamicGraph}, - prelude::GraphViewOps, - search::{into_indexed::DynamicIndexedGraph, IndexedGraph}, -}; #[derive(ResolvedObject)] pub(crate) struct GqlGraphs { - graphs: Vec>, + names: Vec, + namespaces: Vec>, } impl GqlGraphs { - pub fn new(graphs: Vec) -> Self { - Self { - graphs: graphs - .into_iter() - .map(|g| g.into_dynamic_indexed()) - .collect(), - } + pub fn new(names: Vec, namespaces: Vec>) -> Self { + Self { names, namespaces } } } #[ResolvedObjectFields] impl GqlGraphs { - async fn names(&self) -> Vec { - self.graphs - .iter() - .map(|g| g.properties().constant().get("name").unwrap().to_string()) - .collect() + async fn name(&self) -> Result, Error> { + Ok(self.names.clone()) } - async fn properties(&self) -> Vec { - self.graphs - .iter() - .map(|g| Into::::into(g.properties()).into()) - .collect() - } - - async fn unique_layers(&self) -> Vec> { - self.graphs - .iter() - .map(|g| g.unique_layers().map_into().collect()) - .collect() + async fn path(&self) -> Result, Error> { + Ok(self + .names + .clone() + .into_iter() + .zip(self.namespaces.clone().into_iter()) + .map(|(name, namespace)| match namespace { + Some(ns) => format!("{}/{}", ns, name), + None => name, + }) + .collect()) } } diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index 8ca9c5694e..7da84987aa 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -1,12 +1,13 @@ use crate::{ - data::{load_graphs_from_path, Data}, + data::{load_graph_from_path, load_graphs_from_path, Data}, model::{ algorithms::global_plugins::GlobalPlugins, graph::{graph::GqlGraph, graphs::GqlGraphs, vectorised_graph::GqlVectorisedGraph}, }, + url_encode::url_decode_graph, }; use async_graphql::Context; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use base64::{engine::general_purpose::STANDARD, Engine}; use chrono::Utc; use dynamic_graphql::{ App, Mutation, MutationFields, MutationRoot, ResolvedObject, ResolvedObjectFields, Result, @@ -17,7 +18,6 @@ use raphtory::{ core::{utils::errors::GraphError, ArcStr, Prop}, db::api::view::MaterializedGraph, prelude::{GraphViewOps, ImportOps, NodeViewOps, PropertyAdditionOps}, - search::{into_indexed::DynamicIndexedGraph, IndexedGraph}, }; use serde_json::Value; use std::{ @@ -25,7 +25,7 @@ use std::{ fmt::{Display, Formatter}, fs, io::Read, - path::Path, + path::{Path, PathBuf}, }; pub mod algorithms; @@ -49,6 +49,12 @@ pub enum GqlGraphError { ImmutableDiskGraph, #[error("Graph does exists at path {0}")] GraphDoesNotExists(String), + #[error("Failed to load graph")] + FailedToLoadGraph, + #[error("Invalid namespace: {0}")] + InvalidNamespace(String), + #[error("Failed to create dir {0}")] + FailedToCreateDir(String), } #[derive(ResolvedObject)] @@ -62,34 +68,37 @@ impl QueryRoot { } /// Returns a graph - async fn graph<'a>(ctx: &Context<'a>, name: &str) -> Result> { + async fn graph<'a>( + ctx: &Context<'a>, + name: &str, + namespace: &Option, + ) -> Result { let data = ctx.data_unchecked::(); - let graph = data.get_graph(name)?; - match graph { - Some(g) => Ok(Some(GqlGraph::new(name.to_string(), g))), - None => Ok(None), - } + Ok(data + .get_graph(name, namespace) + .map(|g| GqlGraph::new(name.to_string(), namespace.clone(), g))?) } - async fn vectorised_graph<'a>(ctx: &Context<'a>, name: &str) -> Option { + async fn vectorised_graph<'a>( + ctx: &Context<'a>, + name: &str, + namespace: &Option, + ) -> Option { let data = ctx.data_unchecked::(); + let graph_name = construct_graph_name(&name.to_string(), namespace); let g = data .global_plugins .vectorised_graphs .read() - .get(name) + .get(&graph_name) .cloned()?; Some(g.into()) } async fn graphs<'a>(ctx: &Context<'a>) -> Result> { let data = ctx.data_unchecked::(); - let graphs = data - .get_graphs()? - .iter() - .map(|(_, g)| (*g).clone()) - .collect_vec(); - Ok(Some(GqlGraphs::new(graphs))) + let (names, namespaces) = data.get_graph_names_namespaces()?; + Ok(Some(GqlGraphs::new(names, namespaces))) } async fn plugins<'a>(ctx: &Context<'a>) -> GlobalPlugins { @@ -97,11 +106,14 @@ impl QueryRoot { data.global_plugins.clone() } - async fn receive_graph<'a>(ctx: &Context<'a>, name: &str) -> Result { + async fn receive_graph<'a>( + ctx: &Context<'a>, + name: &str, + namespace: &Option, + ) -> Result { let data = ctx.data_unchecked::(); - let g = data.get_graph(name)?.ok_or(MissingGraph)?.materialize()?; - let bincode = bincode::serialize(&g)?; - Ok(URL_SAFE_NO_PAD.encode(bincode)) + let g = data.get_graph(name, namespace)?.materialize()?; + Ok(STANDARD.encode(g.bincode()?)) } } @@ -113,235 +125,295 @@ pub(crate) struct Mut(MutRoot); #[MutationFields] impl Mut { - /// Load graphs from a directory of bincode files (existing graphs with the same name are overwritten) - /// - /// Returns:: - /// list of names for newly added graphs - async fn load_graphs_from_path<'a>( + // If namespace is not provided, it will be set to the current working directory. + async fn delete_graph<'a>( ctx: &Context<'a>, - path: String, - overwrite: bool, - ) -> Result> { + graph_name: String, + graph_namespace: &Option, + ) -> Result { let data = ctx.data_unchecked::(); - let names = load_graphs_from_path(data.work_dir.as_ref(), (&path).as_ref(), overwrite)?; - names.iter().for_each(|name| data.graphs.invalidate(name)); - Ok(names) + let current_graph_path = + construct_graph_path(&data.work_dir, &graph_name, graph_namespace)?; + if !current_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &graph_name, + graph_namespace, + )) + .into()); + } + + delete_graph(¤t_graph_path)?; + data.graphs.remove(&graph_name); + + Ok(true) } - async fn rename_graph<'a>( + // If namespace is not provided, it will be set to the current working directory. + // This applies to both the graph namespace and new graph namespace. + async fn move_graph<'a>( ctx: &Context<'a>, - parent_graph_name: String, graph_name: String, + graph_namespace: &Option, new_graph_name: String, + new_graph_namespace: &Option, ) -> Result { let data = ctx.data_unchecked::(); - if data.graphs.contains_key(&new_graph_name) { + let current_graph_path = + construct_graph_path(&data.work_dir, &graph_name, graph_namespace)?; + if !current_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &graph_name, + graph_namespace, + )) + .into()); + } + let new_graph_path = + construct_graph_path(&data.work_dir, &new_graph_name, new_graph_namespace)?; + if new_graph_path.exists() { return Err(GraphError::GraphNameAlreadyExists { - name: new_graph_name, + name: construct_graph_name(&new_graph_name, new_graph_namespace), } .into()); } - let data = ctx.data_unchecked::(); - let subgraph = data.get_graph(&graph_name)?.ok_or("Graph not found")?; + let current_graph = data.get_graph(&graph_name, graph_namespace)?; #[cfg(feature = "storage")] - if subgraph.clone().graph.into_disk_graph().is_some() { + if current_graph.clone().graph.into_disk_graph().is_some() { return Err(GqlGraphError::ImmutableDiskGraph.into()); } - if new_graph_name.ne(&graph_name) && parent_graph_name.ne(&graph_name) { - let old_path = subgraph - .properties() - .constant() - .get("path") - .ok_or("Path is missing")? - .to_string(); - let old_path = Path::new(&old_path); - let path = old_path - .parent() - .map(|p| p.to_path_buf()) - .ok_or("Path is missing")?; - let path = path.join(&new_graph_name); - - let parent_graph = data - .get_graph(&parent_graph_name)? - .ok_or("Graph not found")?; - let new_subgraph = parent_graph - .subgraph(subgraph.nodes().iter().map(|v| v.name()).collect_vec()) - .materialize()?; - - let static_props_without_name: Vec<(ArcStr, Prop)> = subgraph - .properties() - .into_iter() - .filter(|(a, _)| a != "name") - .collect_vec(); - - new_subgraph.update_constant_properties(static_props_without_name)?; + if new_graph_path.ne(¤t_graph_path) { + let timestamp: i64 = Utc::now().timestamp(); - new_subgraph - .update_constant_properties([("name", Prop::Str(new_graph_name.clone().into()))])?; - new_subgraph.update_constant_properties([( - "path", - Prop::Str(path.display().to_string().into()), - )])?; - - let dt = Utc::now(); - let timestamp: i64 = dt.timestamp(); - new_subgraph + current_graph .update_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; - new_subgraph + current_graph .update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; - new_subgraph.save_to_file(&path)?; - - let gi: IndexedGraph = new_subgraph.into(); + current_graph.save_to_file(&new_graph_path)?; - data.graphs.insert(new_graph_name, gi); + delete_graph(¤t_graph_path)?; data.graphs.remove(&graph_name); - delete_graph(&old_path)?; } Ok(true) } - async fn update_graph_last_opened<'a>(ctx: &Context<'a>, graph_name: String) -> Result { + // If namespace is not provided, it will be set to the current working directory. + // This applies to both the graph namespace and new graph namespace. + async fn copy_graph<'a>( + ctx: &Context<'a>, + graph_name: String, + graph_namespace: &Option, + new_graph_name: String, + new_graph_namespace: &Option, + ) -> Result { + let data = ctx.data_unchecked::(); + let current_graph_path = + construct_graph_path(&data.work_dir, &graph_name, graph_namespace)?; + if !current_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &graph_name, + graph_namespace, + )) + .into()); + } + let new_graph_path = + construct_graph_path(&data.work_dir, &new_graph_name, new_graph_namespace)?; + if new_graph_path.exists() { + return Err(GraphError::GraphNameAlreadyExists { + name: construct_graph_name(&new_graph_name, new_graph_namespace), + } + .into()); + } + + let current_graph = data.get_graph(&graph_name, graph_namespace)?; + + #[cfg(feature = "storage")] + if current_graph.clone().graph.into_disk_graph().is_some() { + return Err(GqlGraphError::ImmutableDiskGraph.into()); + } + + if new_graph_path.ne(¤t_graph_path) { + let timestamp: i64 = Utc::now().timestamp(); + + let new_graph = current_graph + .subgraph(current_graph.nodes().name().collect_vec()) + .materialize()?; + + new_graph.update_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; + new_graph.update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; + + new_graph.save_to_file(&new_graph_path)?; + } + + Ok(true) + } + + async fn update_graph_last_opened<'a>( + ctx: &Context<'a>, + graph_name: String, + namespace: &Option, + ) -> Result { let data = ctx.data_unchecked::(); - let subgraph = data.get_graph(&graph_name)?.ok_or("Graph not found")?; + let graph = data.get_graph(&graph_name, namespace)?; #[cfg(feature = "storage")] - if subgraph.clone().graph.into_disk_graph().is_some() { + if graph.clone().graph.into_disk_graph().is_some() { return Err(GqlGraphError::ImmutableDiskGraph.into()); } let dt = Utc::now(); let timestamp: i64 = dt.timestamp(); - subgraph.update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; + graph.update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; - let path = subgraph - .properties() - .constant() - .get("path") - .ok_or("Path is missing")? - .to_string(); + let path = construct_graph_path(&data.work_dir, &graph_name, namespace)?; + graph.save_to_file(path)?; + data.graphs.insert(graph_name, graph); - subgraph.save_to_file(path)?; - data.graphs.insert(graph_name, subgraph); + Ok(true) + } + + async fn create_graph<'a>( + ctx: &Context<'a>, + parent_graph_name: String, + parent_graph_namespace: &Option, + new_graph_namespace: &Option, + new_graph_name: String, + props: String, + is_archive: u8, + graph_nodes: Vec, + ) -> Result { + let data = ctx.data_unchecked::(); + let parent_graph_path = + construct_graph_path(&data.work_dir, &parent_graph_name, parent_graph_namespace)?; + if !parent_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &parent_graph_name, + parent_graph_namespace, + )) + .into()); + } + + let new_graph_path = + construct_graph_path(&data.work_dir, &new_graph_name, new_graph_namespace)?; + if new_graph_path.exists() { + return Err(GraphError::GraphNameAlreadyExists { + name: construct_graph_name(&new_graph_name, new_graph_namespace), + } + .into()); + } + + let timestamp: i64 = Utc::now().timestamp(); + let node_ids = graph_nodes.iter().map(|key| key.as_str()).collect_vec(); + + // Creating a new graph (owner is user) from UI + // Graph is created from the parent graph. This means the new graph retains the character of the parent graph i.e., + // the new graph is an event or persistent graph depending on if the parent graph is event or persistent graph, respectively. + let parent_graph = data.get_graph(&parent_graph_name, parent_graph_namespace)?; + let new_subgraph = parent_graph.subgraph(node_ids.clone()).materialize()?; + + new_subgraph.update_constant_properties([("creationTime", Prop::I64(timestamp * 1000))])?; + new_subgraph.update_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; + new_subgraph.update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; + new_subgraph.update_constant_properties([("uiProps", Prop::Str(props.into()))])?; + new_subgraph.update_constant_properties([("isArchive", Prop::U8(is_archive))])?; + + new_subgraph.save_to_file(new_graph_path)?; + + data.graphs + .insert(new_graph_name.clone(), new_subgraph.into()); Ok(true) } - async fn save_graph<'a>( + async fn update_graph<'a>( ctx: &Context<'a>, parent_graph_name: String, + parent_graph_namespace: &Option, graph_name: String, + graph_namespace: &Option, new_graph_name: String, props: String, is_archive: u8, - graph_nodes: String, + graph_nodes: Vec, ) -> Result { let data = ctx.data_unchecked::(); + let parent_graph_path = + construct_graph_path(&data.work_dir, &parent_graph_name, parent_graph_namespace)?; + if !parent_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &parent_graph_name, + parent_graph_namespace, + )) + .into()); + } - // If graph_name == new_graph_name, it is a "save" action otherwise it is "save as" action. - // Overwriting the same graph is permitted, not otherwise - if graph_name.ne(&new_graph_name) { - let new_graph_path = Path::new(&data.work_dir).join(&new_graph_name); + // Saving an existing graph + let current_graph_path = + construct_graph_path(&data.work_dir, &graph_name, graph_namespace)?; + if !current_graph_path.exists() { + return Err(GraphError::GraphNotFound(construct_graph_name( + &graph_name, + graph_namespace, + )) + .into()); + } + + let new_graph_path = + construct_graph_path(&data.work_dir, &new_graph_name, graph_namespace)?; + if graph_name != new_graph_name { + // Save as if new_graph_path.exists() { return Err(GraphError::GraphNameAlreadyExists { - name: new_graph_name, + name: construct_graph_name(&new_graph_name, graph_namespace), } .into()); } } - let parent_graph = data - .get_graph(&parent_graph_name)? - .ok_or("Graph not found")?; - let subgraph = data.get_graph(&graph_name)?.ok_or("Graph not found")?; - + let current_graph = data.get_graph(&graph_name, graph_namespace)?; #[cfg(feature = "storage")] - if subgraph.clone().graph.into_disk_graph().is_some() { + if current_graph.clone().graph.into_disk_graph().is_some() { return Err(GqlGraphError::ImmutableDiskGraph.into()); } - let path = match data.get_graph(&new_graph_name)? { - Some(new_graph) => new_graph - .properties() - .constant() - .get("path") - .ok_or("Path is missing")? - .to_string(), - None => { - let base_path = subgraph - .properties() - .constant() - .get("path") - .ok_or("Path is missing")? - .to_string(); - let path: &Path = Path::new(base_path.as_str()); - path.with_file_name(&new_graph_name) - .to_str() - .ok_or("Invalid path")? - .to_string() - } - }; - println!("Saving graph to path {path}"); - - let deserialized_node_map: Value = serde_json::from_str(graph_nodes.as_str())?; - let node_map = deserialized_node_map - .as_object() - .ok_or("graph_nodes not object")?; - let node_ids = node_map.keys().map(|key| key.as_str()).collect_vec(); - - let _new_subgraph = parent_graph.subgraph(node_ids.clone()).materialize()?; - _new_subgraph.update_constant_properties([("name", Prop::str(new_graph_name.clone()))])?; - - let new_subgraph = &_new_subgraph.clone().into_persistent().unwrap(); - let new_subgraph_data = subgraph.subgraph(node_ids).materialize()?; - - // Copy nodes over - let new_subgraph_nodes: Vec<_> = new_subgraph_data - .clone() - .into_persistent() - .unwrap() - .nodes() - .collect(); - let nodeviews = new_subgraph_nodes.iter().map(|node| node).collect(); - new_subgraph.import_nodes(nodeviews, true)?; - - // Copy edges over - let new_subgraph_edges: Vec<_> = new_subgraph_data - .into_persistent() - .unwrap() - .edges() - .collect(); - let edgeviews = new_subgraph_edges.iter().map(|edge| edge).collect(); - new_subgraph.import_edges(edgeviews, true)?; - - // If parent_graph_name == graph_name, it means that the graph is being created from UI - if parent_graph_name.ne(&graph_name) { - // If graph_name == new_graph_name, it is a "save" action otherwise it is "save as" action - if graph_name.ne(&new_graph_name) { - let static_props: Vec<(ArcStr, Prop)> = subgraph - .properties() - .into_iter() - .filter(|(a, _)| a != "name" && a != "creationTime" && a != "uiProps") - .collect_vec(); - new_subgraph.update_constant_properties(static_props)?; - } else { - let static_props: Vec<(ArcStr, Prop)> = subgraph - .properties() - .into_iter() - .filter(|(a, _)| a != "name" && a != "lastUpdated" && a != "uiProps") - .collect_vec(); - new_subgraph.update_constant_properties(static_props)?; - } - } + let timestamp: i64 = Utc::now().timestamp(); + let node_ids = graph_nodes.iter().map(|key| key.as_str()).collect_vec(); - let dt = Utc::now(); - let timestamp: i64 = dt.timestamp(); + // Creating a new graph from the current graph instead of the parent graph preserves the character of the new graph + // i.e., the new graph is an event or persistent graph depending on if the current graph is event or persistent graph, respectively. + let new_subgraph = current_graph.subgraph(node_ids.clone()).materialize()?; + + let parent_graph = data.get_graph(&parent_graph_name, parent_graph_namespace)?; + let new_node_ids = node_ids + .iter() + .filter(|x| current_graph.graph.node(x).is_none()) + .collect_vec(); + let parent_subgraph = parent_graph.subgraph(new_node_ids); + + let nodes = parent_subgraph.nodes(); + new_subgraph.import_nodes(nodes, true)?; + let edges = parent_subgraph.edges(); + new_subgraph.import_edges(edges, true)?; - if parent_graph_name.eq(&graph_name) || graph_name.ne(&new_graph_name) { + if graph_name == new_graph_name { + // Save + let static_props: Vec<(ArcStr, Prop)> = current_graph + .properties() + .into_iter() + .filter(|(a, _)| a != "name" && a != "lastUpdated" && a != "uiProps") + .collect_vec(); + new_subgraph.update_constant_properties(static_props)?; + } else { + // Save as + let static_props: Vec<(ArcStr, Prop)> = current_graph + .properties() + .into_iter() + .filter(|(a, _)| a != "name" && a != "creationTime" && a != "uiProps") + .collect_vec(); + new_subgraph.update_constant_properties(static_props)?; new_subgraph .update_constant_properties([("creationTime", Prop::I64(timestamp * 1000))])?; } @@ -349,30 +421,62 @@ impl Mut { new_subgraph.update_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; new_subgraph.update_constant_properties([("lastOpened", Prop::I64(timestamp * 1000))])?; new_subgraph.update_constant_properties([("uiProps", Prop::Str(props.into()))])?; - new_subgraph.update_constant_properties([("path", Prop::Str(path.clone().into()))])?; new_subgraph.update_constant_properties([("isArchive", Prop::U8(is_archive))])?; - new_subgraph.save_to_file(path)?; - - let m_g = new_subgraph.materialize()?; - let gi: IndexedGraph = m_g.into(); + new_subgraph.save_to_file(new_graph_path)?; - data.graphs.insert(new_graph_name.clone(), gi); + data.graphs.remove(graph_name.as_str()); + data.graphs + .insert(new_graph_name.clone(), new_subgraph.into()); Ok(true) } + /// Load graph from path + /// + /// Returns:: + /// list of names for newly added graphs + async fn load_graph_from_path<'a>( + ctx: &Context<'a>, + file_path: String, + namespace: &Option, + overwrite: bool, + ) -> Result { + let data = ctx.data_unchecked::(); + let name = load_graph_from_path( + data.work_dir.as_ref(), + (&file_path).as_ref(), + namespace, + overwrite, + )?; + data.graphs.invalidate(&name); + Ok(name) + } + /// Use GQL multipart upload to send new graphs to server /// /// Returns:: /// name of the new graph - async fn upload_graph<'a>(ctx: &Context<'a>, name: String, graph: Upload) -> Result { + async fn upload_graph<'a>( + ctx: &Context<'a>, + name: String, + graph: Upload, + namespace: &Option, + overwrite: bool, + ) -> Result { + let data = ctx.data_unchecked::(); + let path = construct_graph_path(data.work_dir.as_str(), name.as_str(), namespace)?; + if path.exists() && !overwrite { + return Err(GraphError::GraphNameAlreadyExists { name }.into()); + } let mut buffer = Vec::new(); let mut buff_read = graph.value(ctx)?.content; buff_read.read_to_end(&mut buffer)?; let g: MaterializedGraph = MaterializedGraph::from_bincode(&buffer)?; - let data = ctx.data_unchecked::(); - g.save_to_file(Path::new(data.work_dir.as_str()).join(name.as_str()))?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(GraphError::from)?; + } + g.save_to_file(&path)?; data.graphs.insert(name.clone(), g.into()); Ok(name) } @@ -381,10 +485,20 @@ impl Mut { /// /// Returns:: /// name of the new graph - async fn send_graph<'a>(ctx: &Context<'a>, name: String, graph: String) -> Result { - let g: MaterializedGraph = bincode::deserialize(&URL_SAFE_NO_PAD.decode(graph)?)?; - let data = ctx.data_unchecked::().clone(); - g.save_to_file(Path::new(data.work_dir.as_str()).join(name.as_str()))?; + async fn send_graph<'a>( + ctx: &Context<'a>, + name: String, + graph: String, + namespace: &Option, + overwrite: bool, + ) -> Result { + let data = ctx.data_unchecked::(); + let path = construct_graph_path(&data.work_dir, &name, namespace)?; + if path.exists() && !overwrite { + return Err(GraphError::GraphNameAlreadyExists { name }.into()); + } + let g: MaterializedGraph = url_decode_graph(graph)?; + g.save_to_file(&path)?; data.graphs.insert(name.clone(), g.into()); Ok(name) } @@ -392,11 +506,11 @@ impl Mut { async fn archive_graph<'a>( ctx: &Context<'a>, graph_name: String, - _parent_graph_name: String, + namespace: &Option, is_archive: u8, ) -> Result { let data = ctx.data_unchecked::(); - let subgraph = data.get_graph(&graph_name)?.ok_or("Graph not found")?; + let subgraph = data.get_graph(&graph_name, namespace)?; #[cfg(feature = "storage")] if subgraph.clone().graph.into_disk_graph().is_some() { @@ -405,18 +519,70 @@ impl Mut { subgraph.update_constant_properties([("isArchive", Prop::U8(is_archive))])?; - let path = subgraph - .properties() - .constant() - .get("path") - .ok_or("Path is missing")? - .to_string(); + let path = construct_graph_path(&data.work_dir, &graph_name, namespace)?; subgraph.save_to_file(path)?; data.graphs.insert(graph_name, subgraph); Ok(true) } + + // This function does not serve use case as yet. Need to decide on the semantics for multi-graphs. + async fn load_graphs_from_path<'a>( + ctx: &Context<'a>, + path: String, + namespace: &Option, + overwrite: bool, + ) -> Result> { + let data = ctx.data_unchecked::(); + let names = load_graphs_from_path( + data.work_dir.as_ref(), + (&path).as_ref(), + namespace, + overwrite, + )?; + names.iter().for_each(|name| data.graphs.invalidate(name)); + Ok(names) + } +} + +pub(crate) fn construct_graph_name(name: &String, namespace: &Option) -> String { + match namespace { + Some(namespace) if !namespace.is_empty() => format!("{}/{}", namespace, name), + _ => name.clone(), + } +} + +pub(crate) fn construct_graph_path( + work_dir: &str, + name: &str, + namespace: &Option, +) -> Result { + let mut path = PathBuf::from(work_dir); + if let Some(ns) = namespace { + if ns.contains("//") { + return Err(GqlGraphError::InvalidNamespace(ns.to_string())); + } + + let ns_path = Path::new(&ns); + for comp in ns_path.components() { + if matches!(comp, std::path::Component::ParentDir) { + return Err(GqlGraphError::InvalidNamespace(ns.to_string())); + } + } + path = path.join(ns_path); + } + path = path.join(name); + + // Check if the path exists, if not create it + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent) + .map_err(|e| GqlGraphError::FailedToCreateDir(e.to_string()))?; + } + } + + Ok(path) } #[derive(App)] diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 31b700a01e..9834af510c 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -25,7 +25,7 @@ use poem::{ EndpointExt, Route, Server, }; use raphtory::{ - db::api::view::{DynamicGraph, IntoDynamic, MaterializedGraph}, + db::api::view::{DynamicGraph, IntoDynamic}, vectors::{ document_template::{DefaultTemplate, DocumentTemplate}, vectorisable::Vectorisable, @@ -35,15 +35,18 @@ use raphtory::{ use crate::{ data::get_graphs_from_work_dir, - server_config::{load_config, AppConfig, AuthConfig, CacheConfig, LoggingConfig}, + server_config::{load_config, AppConfig, LoggingConfig}, }; +use config::ConfigError; use std::{ collections::HashMap, fs, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; +use thiserror::Error; use tokio::{ + io, io::Result as IoResult, signal, sync::{ @@ -52,11 +55,34 @@ use tokio::{ }, task::JoinHandle, }; -use tracing::{metadata::ParseLevelError, Level}; use tracing_subscriber::{ - fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, FmtSubscriber, - Registry, + layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, FmtSubscriber, Registry, }; +use url::ParseError; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("Config error: {0}")] + ConfigError(#[from] ConfigError), + #[error("Cache error: {0}")] + CacheError(String), + #[error("No client id provided")] + MissingClientId, + #[error("No client secret provided")] + MissingClientSecret, + #[error("No tenant id provided")] + MissingTenantId, + #[error("Parse error: {0}")] + FailedToParseUrl(#[from] ParseError), + #[error("Failed to fetch JWKS")] + FailedToFetchJWKS, +} + +impl From for io::Error { + fn from(error: ServerError) -> Self { + io::Error::new(io::ErrorKind::Other, error) + } +} /// A struct for defining and running a Raphtory GraphQL server pub struct RaphtoryServer { @@ -66,23 +92,18 @@ pub struct RaphtoryServer { impl RaphtoryServer { pub fn new( - work_dir: &Path, - graphs: Option>, - graph_paths: Option>, - cache_config: Option, - auth_config: Option, - config_path: Option<&Path>, - ) -> Self { + work_dir: PathBuf, + app_config: Option, + config_path: Option, + ) -> IoResult { if !work_dir.exists() { - fs::create_dir_all(work_dir).unwrap(); + fs::create_dir_all(&work_dir).unwrap(); } - let configs = - load_config(cache_config, auth_config, config_path).expect("Failed to load configs"); + load_config(app_config, config_path).map_err(|err| ServerError::ConfigError(err))?; + let data = Data::new(work_dir.as_path(), &configs); - let data = Data::new(work_dir, graphs, graph_paths, &configs); - - Self { data, configs } + Ok(Self { data, configs }) } /// Vectorise a subset of the graphs of the server. @@ -101,7 +122,7 @@ impl RaphtoryServer { embedding: F, cache: &Path, template: Option, - ) -> Self + ) -> IoResult where F: EmbeddingFunction + Clone + 'static, T: DocumentTemplate + 'static, @@ -110,9 +131,10 @@ impl RaphtoryServer { let graphs = &self.data.global_plugins.graphs; let stores = &self.data.global_plugins.vectorised_graphs; - graphs - .write() - .extend(get_graphs_from_work_dir(work_dir).expect("Failed to load graphs")); + graphs.write().extend( + get_graphs_from_work_dir(work_dir) + .map_err(|err| ServerError::CacheError(err.message))?, + ); let template = template .map(|template| Arc::new(template) as Arc>) @@ -147,7 +169,7 @@ impl RaphtoryServer { } println!("Embeddings were loaded successfully"); - self + Ok(self) } pub fn register_algorithm< @@ -165,11 +187,10 @@ impl RaphtoryServer { /// Start the server on the default port and return a handle to it. pub async fn start( self, - log_level: Option<&str>, enable_tracing: bool, enable_auth: bool, - ) -> RunningRaphtoryServer { - self.start_with_port(1736, log_level, enable_tracing, enable_auth) + ) -> IoResult { + self.start_with_port(1736, enable_tracing, enable_auth) .await } @@ -177,35 +198,10 @@ impl RaphtoryServer { pub async fn start_with_port( self, port: u16, - log_level: Option<&str>, enable_tracing: bool, enable_auth: bool, - ) -> RunningRaphtoryServer { - fn parse_log_level(input: &str) -> Option { - // Parse log level from string - let level: Result = input.trim().parse(); - match level { - Ok(level) => Some(level.to_string()), - Err(_) => None, - } - } - - fn setup_logger_from_loglevel(log_level: String) { - let filter = EnvFilter::try_new(log_level) - .unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); // Default to info if the provided level is invalid - let subscriber = FmtSubscriber::builder() - .with_env_filter(filter) - .with_span_events(FmtSpan::CLOSE) - .finish(); - if let Err(err) = tracing::subscriber::set_global_default(subscriber) { - eprintln!( - "Log level cannot be updated within the same runtime environment: {}", - err - ); - } - } - - fn setup_logger_from_config(configs: &LoggingConfig) { + ) -> IoResult { + fn configure_logger(configs: &LoggingConfig) { let log_level = &configs.log_level; let filter = EnvFilter::new(log_level); let subscriber = FmtSubscriber::builder().with_env_filter(filter).finish(); @@ -217,19 +213,7 @@ impl RaphtoryServer { } } - fn configure_logger(log_level: Option<&str>, configs: &LoggingConfig) { - if let Some(log_level) = log_level { - if let Some(log_level) = parse_log_level(log_level) { - setup_logger_from_loglevel(log_level); - } else { - setup_logger_from_config(configs); - } - } else { - setup_logger_from_config(configs); - } - } - - configure_logger(log_level, &self.configs.logging); + configure_logger(&self.configs.logging); let registry = Registry::default().with(tracing_subscriber::fmt::layer().pretty()); let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("INFO")); @@ -248,9 +232,9 @@ impl RaphtoryServer { let app: CorsEndpoint> = if enable_auth { println!("Generating endpoint with auth"); self.generate_microsoft_endpoint_with_auth(enable_tracing, port) - .await + .await? } else { - self.generate_endpoint(enable_tracing).await + self.generate_endpoint(enable_tracing).await? }; let (signal_sender, signal_receiver) = mpsc::channel(1); @@ -260,16 +244,16 @@ impl RaphtoryServer { .run_with_graceful_shutdown(app, server_termination(signal_receiver), None); let server_result = tokio::spawn(server_task); - RunningRaphtoryServer { + Ok(RunningRaphtoryServer { signal_sender, server_result, - } + }) } async fn generate_endpoint( self, enable_tracing: bool, - ) -> CorsEndpoint> { + ) -> IoResult>> { let schema_builder = App::create_schema(); let schema_builder = schema_builder.data(self.data); let schema = if enable_tracing { @@ -284,14 +268,14 @@ impl RaphtoryServer { .at("/health", get(health)) .with(CookieJarManager::new()) .with(Cors::new()); - app + Ok(app) } async fn generate_microsoft_endpoint_with_auth( self, enable_tracing: bool, port: u16, - ) -> CorsEndpoint> { + ) -> IoResult>> { let schema_builder = App::create_schema(); let schema_builder = schema_builder.data(self.data); let schema = if enable_tracing { @@ -302,13 +286,21 @@ impl RaphtoryServer { }; dotenv().ok(); - let client_id = self.configs.auth.client_id.expect("No client id provided"); + let client_id = self + .configs + .auth + .client_id + .ok_or(ServerError::MissingClientId)?; let client_secret = self .configs .auth .client_secret - .expect("No client secret provided"); - let tenant_id = self.configs.auth.tenant_id.expect("No tenant id provided"); + .ok_or(ServerError::MissingClientSecret)?; + let tenant_id = self + .configs + .auth + .tenant_id + .ok_or(ServerError::MissingTenantId)?; let client_id = ClientId::new(client_id); let client_secret = ClientSecret::new(client_secret); @@ -317,12 +309,12 @@ impl RaphtoryServer { "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize", tenant_id.clone() )) - .expect("Invalid authorization endpoint URL"); + .map_err(|e| ServerError::FailedToParseUrl(e))?; let token_url = TokenUrl::new(format!( "https://login.microsoftonline.com/{}/oauth2/v2.0/token", tenant_id.clone() )) - .expect("Invalid token endpoint URL"); + .map_err(|e| ServerError::FailedToParseUrl(e))?; println!("Loading client"); let client = BasicClient::new( @@ -336,11 +328,13 @@ impl RaphtoryServer { "http://localhost:{}/auth/callback", port.to_string() )) - .expect("Invalid redirect URL"), + .map_err(|e| ServerError::FailedToParseUrl(e))?, ); println!("Fetching JWKS"); - let jwks = get_jwks().await.expect("Failed to fetch JWKS"); + let jwks = get_jwks() + .await + .map_err(|_| ServerError::FailedToFetchJWKS)?; let app_state = AppState { oauth_client: Arc::new(client), @@ -372,37 +366,22 @@ impl RaphtoryServer { .with(CookieJarManager::new()) .with(Cors::new()); println!("App done"); - app + Ok(app) } /// Run the server on the default port until completion. - pub async fn run(self, log_level: Option<&str>, enable_tracing: bool) -> IoResult<()> { - self.start(log_level, enable_tracing, false) - .await - .wait() - .await + pub async fn run(self, enable_tracing: bool) -> IoResult<()> { + self.start(enable_tracing, false).await?.wait().await } - pub async fn run_with_auth( - self, - log_level: Option<&str>, - enable_tracing: bool, - ) -> IoResult<()> { - self.start(log_level, enable_tracing, true) - .await - .wait() - .await + pub async fn run_with_auth(self, enable_tracing: bool) -> IoResult<()> { + self.start(enable_tracing, true).await?.wait().await } /// Run the server on the port `port` until completion. - pub async fn run_with_port( - self, - port: u16, - log_level: Option<&str>, - enable_tracing: bool, - ) -> IoResult<()> { - self.start_with_port(port, log_level, enable_tracing, false) - .await + pub async fn run_with_port(self, port: u16, enable_tracing: bool) -> IoResult<()> { + self.start_with_port(port, enable_tracing, false) + .await? .wait() .await } @@ -475,11 +454,11 @@ mod server_tests { #[tokio::test] async fn test_server_start_stop() { let tmp_dir = tempfile::tempdir().unwrap(); - let server = RaphtoryServer::new(tmp_dir.path(), None, None, None, None, None); - println!("calling start at time {}", Local::now()); - let handler = server.start_with_port(0, None, false, false); + let server = RaphtoryServer::new(tmp_dir.path().to_path_buf(), None, None).unwrap(); + println!("Calling start at time {}", Local::now()); + let handler = server.start_with_port(0, false, false); sleep(Duration::from_secs(1)).await; println!("Calling stop at time {}", Local::now()); - handler.await.stop().await + handler.await.unwrap().stop().await } } diff --git a/raphtory-graphql/src/server_config.rs b/raphtory-graphql/src/server_config.rs index 89b22e4d72..f961594b67 100644 --- a/raphtory-graphql/src/server_config.rs +++ b/raphtory-graphql/src/server_config.rs @@ -1,65 +1,99 @@ use config::{Config, ConfigError, File}; use serde::Deserialize; -use std::path::Path; +use std::path::{Path, PathBuf}; -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct LoggingConfig { pub log_level: String, } -impl Default for LoggingConfig { - fn default() -> Self { - LoggingConfig { - log_level: "INFO".to_string(), - } - } -} - -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct CacheConfig { pub capacity: u64, pub tti_seconds: u64, } -impl Default for CacheConfig { - fn default() -> Self { - CacheConfig { - capacity: 30, - tti_seconds: 900, - } - } -} - -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct AuthConfig { pub client_id: Option, pub client_secret: Option, pub tenant_id: Option, } -impl Default for AuthConfig { - fn default() -> Self { - AuthConfig { - client_id: None, - client_secret: None, - tenant_id: None, - } - } -} - -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct AppConfig { pub logging: LoggingConfig, pub cache: CacheConfig, pub auth: AuthConfig, } -impl Default for AppConfig { - fn default() -> Self { +pub struct AppConfigBuilder { + logging: LoggingConfig, + cache: CacheConfig, + auth: AuthConfig, +} + +impl AppConfigBuilder { + pub fn new() -> Self { + Self { + logging: LoggingConfig { + log_level: "INFO".to_string(), + }, + cache: CacheConfig { + capacity: 30, + tti_seconds: 900, + }, + auth: AuthConfig { + client_id: None, + client_secret: None, + tenant_id: None, + }, + } + } + + pub fn from(config: AppConfig) -> Self { + Self { + logging: config.logging, + cache: config.cache, + auth: config.auth, + } + } + + pub fn with_log_level(mut self, log_level: String) -> Self { + self.logging.log_level = log_level; + self + } + + pub fn with_cache_capacity(mut self, cache_capacity: u64) -> Self { + self.cache.capacity = cache_capacity; + self + } + + pub fn with_cache_tti_seconds(mut self, tti_seconds: u64) -> Self { + self.cache.tti_seconds = tti_seconds; + self + } + + pub fn with_auth_client_id(mut self, client_id: String) -> Self { + self.auth.client_id = Some(client_id); + self + } + + pub fn with_auth_client_secret(mut self, client_secret: String) -> Self { + self.auth.client_secret = Some(client_secret); + self + } + + pub fn with_auth_tenant_id(mut self, tenant_id: String) -> Self { + self.auth.tenant_id = Some(tenant_id); + self + } + + pub fn build(self) -> AppConfig { AppConfig { - logging: Default::default(), - cache: Default::default(), - auth: Default::default(), + logging: self.logging, + cache: self.cache, + auth: self.auth, } } } @@ -69,44 +103,42 @@ impl Default for AppConfig { // This would cause configs from config paths to be ignored. The reason it has been implemented so is to avoid having to pass all the configs as // args from the python instance i.e., being able to provide configs from config path as default configs and yet give precedence to config args. pub fn load_config( - cache_config: Option, - auth_config: Option, - config_path: Option<&Path>, + app_config: Option, + config_path: Option, ) -> Result { - let mut config_builder = Config::builder(); + let mut settings_config_builder = Config::builder(); if let Some(config_path) = config_path { - config_builder = config_builder.add_source(File::from(config_path)); + settings_config_builder = settings_config_builder.add_source(File::from(config_path)); } - let settings = config_builder.build()?; + let settings = settings_config_builder.build()?; - // Load default configs - let mut loaded_config = AppConfig::default(); + let mut app_config_builder = if let Some(app_config) = app_config { + AppConfigBuilder::from(app_config) + } else { + AppConfigBuilder::new() + }; // Override with provided configs from config file if any if let Some(log_level) = settings.get::("logging.log_level").ok() { - loaded_config.logging.log_level = log_level; + app_config_builder = app_config_builder.with_log_level(log_level); } - if let Some(capacity) = settings.get::("cache.capacity").ok() { - loaded_config.cache.capacity = capacity; + if let Some(cache_capacity) = settings.get::("cache.capacity").ok() { + app_config_builder = app_config_builder.with_cache_capacity(cache_capacity); } - if let Some(tti_seconds) = settings.get::("cache.tti_seconds").ok() { - loaded_config.cache.tti_seconds = tti_seconds; + if let Some(cache_tti_seconds) = settings.get::("cache.tti_seconds").ok() { + app_config_builder = app_config_builder.with_cache_tti_seconds(cache_tti_seconds); } - loaded_config.auth.client_id = settings.get::("auth.client_id").ok(); - loaded_config.auth.client_secret = settings.get::("auth.client_secret").ok(); - loaded_config.auth.tenant_id = settings.get::("auth.tenant_id").ok(); - - // Override with provided cache configs if any - if let Some(cache_config) = cache_config { - loaded_config.cache = cache_config; + if let Some(client_id) = settings.get::("auth.client_id").ok() { + app_config_builder = app_config_builder.with_auth_client_id(client_id); } - - // Override with provided auth configs if any - if let Some(auth_config) = auth_config { - loaded_config.auth = auth_config; + if let Some(client_secret) = settings.get::("auth.client_secret").ok() { + app_config_builder = app_config_builder.with_auth_client_secret(client_secret); + } + if let Some(tenant_id) = settings.get::("auth.tenant_id").ok() { + app_config_builder = app_config_builder.with_auth_tenant_id(tenant_id); } - Ok(loaded_config) + Ok(app_config_builder.build()) } #[cfg(test)] @@ -116,7 +148,6 @@ mod tests { #[test] fn test_load_config_from_toml() { - // Prepare a test TOML configuration file let config_toml = r#" [logging] log_level = "DEBUG" @@ -124,21 +155,15 @@ mod tests { [cache] tti_seconds = 1000 "#; - let config_path = Path::new("test_config.toml"); - fs::write(config_path, config_toml).unwrap(); + let config_path = PathBuf::from("test_config.toml"); + fs::write(&config_path, config_toml).unwrap(); - // Load config using the test TOML file - let result = load_config(None, None, Some(config_path)); - let expected_config = AppConfig { - logging: LoggingConfig { - log_level: "DEBUG".to_string(), - }, - cache: CacheConfig { - capacity: 30, - tti_seconds: 1000, - }, - auth: AuthConfig::default(), - }; + let result = load_config(None, Some(config_path.clone())); + let expected_config = AppConfigBuilder::new() + .with_log_level("DEBUG".to_string()) + .with_cache_capacity(30) + .with_cache_tti_seconds(1000) + .build(); assert_eq!(result.unwrap(), expected_config); @@ -148,54 +173,26 @@ mod tests { #[test] fn test_load_config_with_custom_cache() { - // Prepare a custom cache configuration - let custom_cache = CacheConfig { - capacity: 50, - tti_seconds: 1200, - }; - - // Load config with custom cache configuration - let result = load_config(Some(custom_cache), None, None); - let expected_config = AppConfig { - logging: LoggingConfig { - log_level: "INFO".to_string(), - }, // Default logging level - cache: CacheConfig { - capacity: 50, - tti_seconds: 1200, - }, - auth: AuthConfig::default(), - }; + let app_config = AppConfigBuilder::new() + .with_cache_capacity(50) + .with_cache_tti_seconds(1200) + .build(); - assert_eq!(result.unwrap(), expected_config); + let result = load_config(Some(app_config.clone()), None); + + assert_eq!(result.unwrap(), app_config); } #[test] fn test_load_config_with_custom_auth() { - // Prepare a custom cache configuration - let custom_auth = AuthConfig { - client_id: Some("custom_client_id".to_string()), - client_secret: Some("custom_client_secret".to_string()), - tenant_id: Some("custom_tenant_id".to_string()), - }; - - // Load config with custom cache configuration - let result = load_config(None, Some(custom_auth), None); - let expected_config = AppConfig { - logging: LoggingConfig { - log_level: "INFO".to_string(), - }, // Default logging level - cache: CacheConfig { - capacity: 30, - tti_seconds: 900, - }, - auth: AuthConfig { - client_id: Some("custom_client_id".to_string()), - client_secret: Some("custom_client_secret".to_string()), - tenant_id: Some("custom_tenant_id".to_string()), - }, - }; + let app_config = AppConfigBuilder::new() + .with_auth_client_id("custom_client_id".to_string()) + .with_auth_client_secret("custom_client_secret".to_string()) + .with_auth_tenant_id("custom_tenant_id".to_string()) + .build(); - assert_eq!(result.unwrap(), expected_config); + let result = load_config(Some(app_config.clone()), None); + + assert_eq!(result.unwrap(), app_config); } } diff --git a/raphtory-graphql/src/url_encode.rs b/raphtory-graphql/src/url_encode.rs new file mode 100644 index 0000000000..c51688a011 --- /dev/null +++ b/raphtory-graphql/src/url_encode.rs @@ -0,0 +1,27 @@ +use base64::{prelude::BASE64_URL_SAFE, DecodeError, Engine}; +use raphtory::{core::utils::errors::GraphError, db::api::view::MaterializedGraph}; + +#[derive(thiserror::Error, Debug)] +pub enum UrlDecodeError { + #[error("Bincode operation failed")] + GraphError { + #[from] + source: GraphError, + }, + #[error("Base64 decoding failed")] + DecodeError { + #[from] + source: DecodeError, + }, +} + +pub fn url_encode_graph>(graph: G) -> Result { + let g: MaterializedGraph = graph.into(); + Ok(BASE64_URL_SAFE.encode(g.bincode()?)) +} + +pub fn url_decode_graph>(graph: T) -> Result { + Ok(MaterializedGraph::from_bincode( + &BASE64_URL_SAFE.decode(graph)?, + )?) +} diff --git a/raphtory/src/core/utils/errors.rs b/raphtory/src/core/utils/errors.rs index ce5fe09555..ccf15a6ffd 100644 --- a/raphtory/src/core/utils/errors.rs +++ b/raphtory/src/core/utils/errors.rs @@ -1,4 +1,5 @@ use crate::core::{utils::time::error::ParseTimeError, ArcStr, Prop, PropType}; +use std::path::PathBuf; #[cfg(feature = "search")] use tantivy; #[cfg(feature = "search")] @@ -6,8 +7,18 @@ use tantivy::query::QueryParserError; #[derive(thiserror::Error, Debug)] pub enum GraphError { + #[error("Invalid path: {0:?}")] + InvalidPath(PathBuf), #[error("Graph error occurred")] UnsupportedDataType, + #[error("Disk graph not found")] + DiskGraphNotFound, + #[error("Disk Graph is immutable")] + ImmutableDiskGraph, + #[error("Event Graph doesn't support deletions")] + EventGraphDeletionsNotSupported, + #[error("Graph not found {0}")] + GraphNotFound(String), #[error("Graph already exists by name = {name}")] GraphNameAlreadyExists { name: String }, #[error("Immutable graph reference already exists. You can access mutable graph apis only exclusively.")] @@ -109,7 +120,7 @@ pub enum GraphError { }, #[error( - "Failed to load the graph as the bincode version {0} is different to installed version {1}" + "Failed to load the graph as the bincode version {0} is different to supported version {1}" )] BincodeVersionError(u32, u32), diff --git a/raphtory/src/db/api/mutation/import_ops.rs b/raphtory/src/db/api/mutation/import_ops.rs index 8ad52c0070..efca8d6791 100644 --- a/raphtory/src/db/api/mutation/import_ops.rs +++ b/raphtory/src/db/api/mutation/import_ops.rs @@ -17,8 +17,9 @@ use crate::{ }, graph::{edge::EdgeView, node::NodeView}, }, - prelude::{AdditionOps, EdgeViewOps, NodeViewOps}, + prelude::{AdditionOps, EdgeViewOps, GraphViewOps, NodeViewOps}, }; +use std::borrow::Borrow; use super::time_from_input; @@ -43,7 +44,7 @@ pub trait ImportOps: /// # Returns /// /// A `Result` which is `Ok` if the node was successfully imported, and `Err` otherwise. - fn import_node( + fn import_node<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, node: &NodeView, force: bool, @@ -63,11 +64,11 @@ pub trait ImportOps: /// # Returns /// /// A `Result` which is `Ok` if the nodes were successfully imported, and `Err` otherwise. - fn import_nodes( + fn import_nodes<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, - node: Vec<&NodeView>, + nodes: impl IntoIterator>>, force: bool, - ) -> Result>, GraphError>; + ) -> Result<(), GraphError>; /// Imports a single edge into the graph. /// @@ -83,7 +84,7 @@ pub trait ImportOps: /// # Returns /// /// A `Result` which is `Ok` if the edge was successfully imported, and `Err` otherwise. - fn import_edge( + fn import_edge<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edge: &EdgeView, force: bool, @@ -103,11 +104,11 @@ pub trait ImportOps: /// # Returns /// /// A `Result` which is `Ok` if the edges were successfully imported, and `Err` otherwise. - fn import_edges( + fn import_edges<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, - edges: Vec<&EdgeView>, + edges: impl IntoIterator>>, force: bool, - ) -> Result>, GraphError>; + ) -> Result<(), GraphError>; } impl< @@ -118,7 +119,7 @@ impl< + InternalMaterialize, > ImportOps for G { - fn import_node( + fn import_node<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, node: &NodeView, force: bool, @@ -169,20 +170,18 @@ impl< Ok(self.node(node.id()).unwrap()) } - fn import_nodes( + fn import_nodes<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, - nodes: Vec<&NodeView>, + nodes: impl IntoIterator>>, force: bool, - ) -> Result>, GraphError> { - let mut added_nodes = vec![]; + ) -> Result<(), GraphError> { for node in nodes { - let res = self.import_node(node, force); - added_nodes.push(res.unwrap()) + self.import_node(node.borrow(), force)?; } - Ok(added_nodes) + Ok(()) } - fn import_edge( + fn import_edge<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, edge: &EdgeView, force: bool, @@ -233,16 +232,14 @@ impl< Ok(self.edge(edge.src().name(), edge.dst().name()).unwrap()) } - fn import_edges( + fn import_edges<'a, GHH: GraphViewOps<'a>, GH: GraphViewOps<'a>>( &self, - edges: Vec<&EdgeView>, + edges: impl IntoIterator>>, force: bool, - ) -> Result>, GraphError> { - let mut added_edges = vec![]; + ) -> Result<(), GraphError> { for edge in edges { - let res = self.import_edge(edge, force); - added_edges.push(res.unwrap()) + self.import_edge(edge.borrow(), force)?; } - Ok(added_edges) + Ok(()) } } diff --git a/raphtory/src/db/api/view/internal/materialize.rs b/raphtory/src/db/api/view/internal/materialize.rs index 8b54110f0e..a349664b24 100644 --- a/raphtory/src/db/api/view/internal/materialize.rs +++ b/raphtory/src/db/api/view/internal/materialize.rs @@ -8,12 +8,17 @@ use crate::{ LayerIds, EID, ELID, VID, }, storage::{locked_view::LockedView, timeindex::TimeIndexEntry}, - utils::errors::GraphError, + utils::errors::{ + GraphError, + GraphError::{EventGraphDeletionsNotSupported, ImmutableDiskGraph}, + }, ArcStr, PropType, }, db::{ api::{ - mutation::internal::{InternalAdditionOps, InternalPropertyAdditionOps}, + mutation::internal::{ + InternalAdditionOps, InternalDeletionOps, InternalPropertyAdditionOps, + }, properties::internal::{ ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps, }, @@ -28,9 +33,11 @@ use crate::{ }, storage_ops::GraphStorage, }, - view::{internal::*, BoxedIter}, + view::{internal::*, BoxedIter, StaticGraphViewOps}, + }, + graph::{ + edge::EdgeView, graph::Graph, node::NodeView, views::deletion_graph::PersistentGraph, }, - graph::{graph::Graph, views::deletion_graph::PersistentGraph}, }, prelude::*, BINCODE_VERSION, @@ -132,8 +139,8 @@ impl MaterializedGraph { } pub fn save_to_file>(&self, path: P) -> Result<(), GraphError> { - let f = std::fs::File::create(path)?; - let mut writer = std::io::BufWriter::new(f); + let f = fs::File::create(path)?; + let mut writer = io::BufWriter::new(f); let versioned_data = VersionedGraph { version: BINCODE_VERSION, graph: self.clone(), @@ -175,6 +182,23 @@ impl MaterializedGraph { } } +impl InternalDeletionOps for MaterializedGraph { + fn internal_delete_edge( + &self, + t: TimeIndexEntry, + src: VID, + dst: VID, + layer: usize, + ) -> Result<(), GraphError> { + match self { + MaterializedGraph::EventGraph(g) => Err(EventGraphDeletionsNotSupported), + MaterializedGraph::PersistentGraph(g) => g.internal_delete_edge(t, src, dst, layer), + #[cfg(feature = "storage")] + MaterializedGraph::DiskEventGraph(g) => Err(ImmutableDiskGraph), + } + } +} + #[enum_dispatch] pub trait InternalMaterialize { fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph; diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs index 4153685334..39486f9398 100644 --- a/raphtory/src/db/graph/graph.rs +++ b/raphtory/src/db/graph/graph.rs @@ -470,8 +470,7 @@ mod db_tests { let gg = Graph::new(); let res = gg.import_nodes(vec![&g_a, &g_b], false).unwrap(); - assert_eq!(res.len(), 2); - assert_eq!(res.iter().map(|n| n.name()).collect_vec(), vec!["A", "B"]); + assert_eq!(gg.nodes().name().collect_vec(), vec!["A", "B"]); let e_a_b = g.add_edge(2, "A", "B", NO_PROPS, None).unwrap(); let res = gg.import_edge(&e_a_b, false).unwrap(); @@ -496,7 +495,7 @@ mod db_tests { let e_c_d = g.add_edge(4, "C", "D", NO_PROPS, None).unwrap(); let gg = Graph::new(); let res = gg.import_edges(vec![&e_a_b, &e_c_d], false).unwrap(); - assert_eq!(res.len(), 2); + assert_eq!(gg.edges().len(), 2); } #[test] diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 455f3ad28c..7d9f280cc3 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -120,7 +120,9 @@ pub mod prelude { }; } +// Upgrade this version number every time you make a breaking change to Graph structure. pub const BINCODE_VERSION: u32 = 1u32; + #[cfg(feature = "storage")] pub use polars_arrow as arrow2; diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs index 4347b87ad8..6b928baf43 100644 --- a/raphtory/src/python/graph/graph.rs +++ b/raphtory/src/python/graph/graph.rs @@ -243,16 +243,10 @@ impl PyGraph { /// nodes (List(Node))- A vector of PyNode objects representing the nodes to be imported. /// force (boolean) - An optional boolean flag indicating whether to force the import of the nodes. /// - /// Returns: - /// Result), GraphError> - A Result object which is Ok if the nodes were successfully imported, and Err otherwise. #[pyo3(signature = (nodes, force = false))] - pub fn import_nodes( - &self, - nodes: Vec, - force: bool, - ) -> Result>, GraphError> { - let nodeviews = nodes.iter().map(|node| &node.node).collect(); - self.graph.import_nodes(nodeviews, force) + pub fn import_nodes(&self, nodes: Vec, force: bool) -> Result<(), GraphError> { + let node_views = nodes.iter().map(|node| &node.node); + self.graph.import_nodes(node_views, force) } /// Import a single edge into the graph. @@ -286,16 +280,10 @@ impl PyGraph { /// edges (List(edges)) - A vector of PyEdge objects representing the edges to be imported. /// force (boolean) - An optional boolean flag indicating whether to force the import of the edges. /// - /// Returns: - /// Result), GraphError> - A Result object which is Ok if the edges were successfully imported, and Err otherwise. #[pyo3(signature = (edges, force = false))] - pub fn import_edges( - &self, - edges: Vec, - force: bool, - ) -> Result>, GraphError> { - let edgeviews = edges.iter().map(|edge| &edge.edge).collect(); - self.graph.import_edges(edgeviews, force) + pub fn import_edges(&self, edges: Vec, force: bool) -> Result<(), GraphError> { + let edge_views = edges.iter().map(|edge| &edge.edge); + self.graph.import_edges(edge_views, force) } //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! @@ -366,6 +354,13 @@ impl PyGraph { Ok(PyBytes::new(py, &bytes)) } + /// Creates a graph from a bincode encoded graph + #[staticmethod] + fn from_bincode(bytes: &[u8]) -> Result, GraphError> { + let graph = MaterializedGraph::from_bincode(bytes)?; + Ok(graph.into_events()) + } + /// Gives the large connected component of a graph. /// /// # Example Usage: diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs index a53c0cebee..b054a6e7b0 100644 --- a/raphtory/src/python/graph/graph_with_deletions.rs +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -14,7 +14,7 @@ use crate::{ }, graph::{edge::EdgeView, node::NodeView, views::deletion_graph::PersistentGraph}, }, - prelude::{DeletionOps, GraphViewOps, ImportOps}, + prelude::{DeletionOps, Graph, GraphViewOps, ImportOps}, python::{ graph::{edge::PyEdge, node::PyNode, views::graph_view::PyGraphView}, utils::{PyInputNode, PyTime}, @@ -277,16 +277,10 @@ impl PyPersistentGraph { /// nodes (List(Node))- A vector of PyNode objects representing the nodes to be imported. /// force (boolean) - An optional boolean flag indicating whether to force the import of the nodes. /// - /// Returns: - /// Result), GraphError> - A Result object which is Ok if the nodes were successfully imported, and Err otherwise. #[pyo3(signature = (nodes, force = false))] - pub fn import_nodes( - &self, - nodes: Vec, - force: bool, - ) -> Result>, GraphError> { - let nodeviews = nodes.iter().map(|node| &node.node).collect(); - self.graph.import_nodes(nodeviews, force) + pub fn import_nodes(&self, nodes: Vec, force: bool) -> Result<(), GraphError> { + let node_views = nodes.iter().map(|node| &node.node); + self.graph.import_nodes(node_views, force) } /// Import a single edge into the graph. @@ -320,16 +314,10 @@ impl PyPersistentGraph { /// edges (List(edges)) - A vector of PyEdge objects representing the edges to be imported. /// force (boolean) - An optional boolean flag indicating whether to force the import of the edges. /// - /// Returns: - /// Result), GraphError> - A Result object which is Ok if the edges were successfully imported, and Err otherwise. #[pyo3(signature = (edges, force = false))] - pub fn import_edges( - &self, - edges: Vec, - force: bool, - ) -> Result>, GraphError> { - let edgeviews = edges.iter().map(|edge| &edge.edge).collect(); - self.graph.import_edges(edgeviews, force) + pub fn import_edges(&self, edges: Vec, force: bool) -> Result<(), GraphError> { + let edge_views = edges.iter().map(|edge| &edge.edge); + self.graph.import_edges(edge_views, force) } //****** Saving And Loading ******// @@ -375,6 +363,13 @@ impl PyPersistentGraph { Ok(PyBytes::new(py, &bytes)) } + /// Creates a graph from a bincode encoded graph + #[staticmethod] + fn from_bincode(bytes: &[u8]) -> Result, GraphError> { + let graph = MaterializedGraph::from_bincode(bytes)?; + Ok(graph.into_persistent()) + } + /// Get event graph pub fn event_graph<'py>(&'py self) -> PyResult> { PyGraph::py_from_db_graph(self.graph.event_graph())