anywidget-graph 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anywidget_graph/__init__.py +1 -1
- anywidget_graph/backends/__init__.py +71 -0
- anywidget_graph/backends/arango.py +201 -0
- anywidget_graph/backends/grafeo.py +70 -0
- anywidget_graph/backends/ladybug.py +94 -0
- anywidget_graph/backends/neo4j.py +143 -0
- anywidget_graph/converters/__init__.py +35 -0
- anywidget_graph/converters/base.py +77 -0
- anywidget_graph/converters/common.py +92 -0
- anywidget_graph/converters/cypher.py +107 -0
- anywidget_graph/converters/gql.py +15 -0
- anywidget_graph/converters/graphql.py +208 -0
- anywidget_graph/converters/gremlin.py +159 -0
- anywidget_graph/converters/sparql.py +167 -0
- anywidget_graph/ui/__init__.py +19 -0
- anywidget_graph/ui/icons.js +15 -0
- anywidget_graph/ui/index.js +199 -0
- anywidget_graph/ui/neo4j.js +193 -0
- anywidget_graph/ui/properties.js +137 -0
- anywidget_graph/ui/schema.js +178 -0
- anywidget_graph/ui/settings.js +299 -0
- anywidget_graph/ui/styles.css +584 -0
- anywidget_graph/ui/toolbar.js +106 -0
- anywidget_graph/widget.py +417 -226
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/METADATA +4 -3
- anywidget_graph-0.2.1.dist-info/RECORD +28 -0
- anywidget_graph-0.1.0.dist-info/RECORD +0 -6
- {anywidget_graph-0.1.0.dist-info → anywidget_graph-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Converter for SPARQL query results (RDF triples)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from anywidget_graph.converters.base import GraphData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SPARQLConverter:
|
|
11
|
+
"""Convert SPARQL query results to graph data.
|
|
12
|
+
|
|
13
|
+
Handles RDF data from SPARQL endpoints:
|
|
14
|
+
- SELECT queries with ?s ?p ?o bindings
|
|
15
|
+
- CONSTRUCT queries returning triples
|
|
16
|
+
- RDFLib result objects
|
|
17
|
+
|
|
18
|
+
The converter treats URIs as node identifiers and predicates as edge labels.
|
|
19
|
+
Literal values become node properties.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
subject_var : str
|
|
24
|
+
Variable name for subjects in SELECT results (default: "s").
|
|
25
|
+
predicate_var : str
|
|
26
|
+
Variable name for predicates in SELECT results (default: "p").
|
|
27
|
+
object_var : str
|
|
28
|
+
Variable name for objects in SELECT results (default: "o").
|
|
29
|
+
node_label_predicate : str | None
|
|
30
|
+
Predicate URI used for node labels (default: rdfs:label).
|
|
31
|
+
Set to None to disable label extraction.
|
|
32
|
+
|
|
33
|
+
Example
|
|
34
|
+
-------
|
|
35
|
+
>>> from rdflib import Graph as RDFGraph
|
|
36
|
+
>>> g = RDFGraph()
|
|
37
|
+
>>> g.parse("data.ttl")
|
|
38
|
+
>>> result = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
|
39
|
+
>>> converter = SPARQLConverter()
|
|
40
|
+
>>> data = converter.convert(result)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
subject_var: str = "s",
|
|
46
|
+
predicate_var: str = "p",
|
|
47
|
+
object_var: str = "o",
|
|
48
|
+
node_label_predicate: str | None = "http://www.w3.org/2000/01/rdf-schema#label",
|
|
49
|
+
) -> None:
|
|
50
|
+
self.subject_var = subject_var
|
|
51
|
+
self.predicate_var = predicate_var
|
|
52
|
+
self.object_var = object_var
|
|
53
|
+
self.node_label_predicate = node_label_predicate
|
|
54
|
+
|
|
55
|
+
def convert(self, result: Any) -> GraphData:
|
|
56
|
+
"""Convert SPARQL result to nodes and edges.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
result : Any
|
|
61
|
+
SPARQL query result (e.g., from RDFLib or SPARQLWrapper).
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
GraphData
|
|
66
|
+
Dictionary with 'nodes' and 'edges' lists.
|
|
67
|
+
"""
|
|
68
|
+
nodes: dict[str, dict[str, Any]] = {}
|
|
69
|
+
edges: list[dict[str, Any]] = []
|
|
70
|
+
labels: dict[str, str] = {} # URI -> label mapping
|
|
71
|
+
|
|
72
|
+
bindings = list(self._iter_bindings(result))
|
|
73
|
+
|
|
74
|
+
# First pass: collect labels
|
|
75
|
+
if self.node_label_predicate:
|
|
76
|
+
for binding in bindings:
|
|
77
|
+
pred = self._get_value(binding, self.predicate_var)
|
|
78
|
+
if pred == self.node_label_predicate:
|
|
79
|
+
subj = self._get_value(binding, self.subject_var)
|
|
80
|
+
obj = self._get_value(binding, self.object_var)
|
|
81
|
+
if subj and obj:
|
|
82
|
+
labels[subj] = obj
|
|
83
|
+
|
|
84
|
+
# Second pass: build graph
|
|
85
|
+
for binding in bindings:
|
|
86
|
+
subj = self._get_value(binding, self.subject_var)
|
|
87
|
+
pred = self._get_value(binding, self.predicate_var)
|
|
88
|
+
obj = self._get_value(binding, self.object_var)
|
|
89
|
+
|
|
90
|
+
if not subj or not pred:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Skip label triples (already processed)
|
|
94
|
+
if pred == self.node_label_predicate:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Create subject node
|
|
98
|
+
if subj not in nodes:
|
|
99
|
+
nodes[subj] = {
|
|
100
|
+
"id": subj,
|
|
101
|
+
"label": labels.get(subj, self._extract_local_name(subj)),
|
|
102
|
+
"uri": subj,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# If object is a URI, create object node and edge
|
|
106
|
+
if obj and self._is_uri(obj):
|
|
107
|
+
if obj not in nodes:
|
|
108
|
+
nodes[obj] = {
|
|
109
|
+
"id": obj,
|
|
110
|
+
"label": labels.get(obj, self._extract_local_name(obj)),
|
|
111
|
+
"uri": obj,
|
|
112
|
+
}
|
|
113
|
+
edges.append(
|
|
114
|
+
{
|
|
115
|
+
"source": subj,
|
|
116
|
+
"target": obj,
|
|
117
|
+
"label": self._extract_local_name(pred),
|
|
118
|
+
"predicate": pred,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
elif obj:
|
|
122
|
+
# Literal value - add as node property
|
|
123
|
+
prop_name = self._extract_local_name(pred)
|
|
124
|
+
nodes[subj][prop_name] = obj
|
|
125
|
+
|
|
126
|
+
return {"nodes": list(nodes.values()), "edges": edges}
|
|
127
|
+
|
|
128
|
+
def _iter_bindings(self, result: Any):
|
|
129
|
+
"""Iterate over result bindings."""
|
|
130
|
+
# RDFLib Result
|
|
131
|
+
if hasattr(result, "bindings"):
|
|
132
|
+
yield from result.bindings
|
|
133
|
+
# SPARQLWrapper JSON result
|
|
134
|
+
elif isinstance(result, dict) and "results" in result:
|
|
135
|
+
yield from result["results"].get("bindings", [])
|
|
136
|
+
# Iterable of bindings
|
|
137
|
+
elif hasattr(result, "__iter__"):
|
|
138
|
+
yield from result
|
|
139
|
+
|
|
140
|
+
def _get_value(self, binding: Any, var: str) -> str:
|
|
141
|
+
"""Extract value from a binding for a variable."""
|
|
142
|
+
if isinstance(binding, dict):
|
|
143
|
+
# SPARQLWrapper format: {"var": {"value": "..."}}
|
|
144
|
+
val = binding.get(var, {})
|
|
145
|
+
if isinstance(val, dict):
|
|
146
|
+
return str(val.get("value", ""))
|
|
147
|
+
return str(val) if val else ""
|
|
148
|
+
# RDFLib format: binding[var] is an RDF term
|
|
149
|
+
elif hasattr(binding, "__getitem__"):
|
|
150
|
+
try:
|
|
151
|
+
val = binding[var]
|
|
152
|
+
return str(val) if val else ""
|
|
153
|
+
except (KeyError, TypeError):
|
|
154
|
+
return ""
|
|
155
|
+
return ""
|
|
156
|
+
|
|
157
|
+
def _is_uri(self, value: str) -> bool:
|
|
158
|
+
"""Check if a value looks like a URI."""
|
|
159
|
+
return value.startswith(("http://", "https://", "urn:"))
|
|
160
|
+
|
|
161
|
+
def _extract_local_name(self, uri: str) -> str:
|
|
162
|
+
"""Extract local name from URI (part after # or last /)."""
|
|
163
|
+
if "#" in uri:
|
|
164
|
+
return uri.split("#")[-1]
|
|
165
|
+
if "/" in uri:
|
|
166
|
+
return uri.rsplit("/", 1)[-1]
|
|
167
|
+
return uri
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""UI components for anywidget-graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
__all__ = ["get_esm", "get_css"]
|
|
8
|
+
|
|
9
|
+
_UI_DIR = Path(__file__).parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_esm() -> str:
|
|
13
|
+
"""Get the combined ESM JavaScript for the widget."""
|
|
14
|
+
return (_UI_DIR / "index.js").read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_css() -> str:
|
|
18
|
+
"""Get the combined CSS styles for the widget."""
|
|
19
|
+
return (_UI_DIR / "styles.css").read_text(encoding="utf-8")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG icons for the graph widget UI.
|
|
3
|
+
*/
|
|
4
|
+
export const ICONS = {
|
|
5
|
+
play: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
|
|
6
|
+
settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4m0 14v4m11-11h-4M5 12H1m17.36-5.64l-2.83 2.83M9.47 14.53l-2.83 2.83m0-10.72l2.83 2.83m5.06 5.06l2.83 2.83"/></svg>`,
|
|
7
|
+
close: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>`,
|
|
8
|
+
chevron: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>`,
|
|
9
|
+
chevronRight: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>`,
|
|
10
|
+
node: `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="8"/></svg>`,
|
|
11
|
+
edge: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14m-4-4l4 4-4 4"/></svg>`,
|
|
12
|
+
property: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>`,
|
|
13
|
+
sidebar: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>`,
|
|
14
|
+
refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>`,
|
|
15
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the anywidget-graph UI.
|
|
3
|
+
* Orchestrates all UI components and graph rendering.
|
|
4
|
+
*/
|
|
5
|
+
import Graph from "https://esm.sh/graphology@0.25.4";
|
|
6
|
+
import Sigma from "https://esm.sh/sigma@3.0.0";
|
|
7
|
+
|
|
8
|
+
import { createToolbar } from "./toolbar.js";
|
|
9
|
+
import { createSchemaPanel } from "./schema.js";
|
|
10
|
+
import { createSettingsPanel } from "./settings.js";
|
|
11
|
+
import { createPropertiesPanel } from "./properties.js";
|
|
12
|
+
import * as neo4jBackend from "./neo4j.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute a query based on the current backend.
|
|
16
|
+
*/
|
|
17
|
+
async function executeQuery(model) {
|
|
18
|
+
const backend = model.get("database_backend");
|
|
19
|
+
const query = model.get("query");
|
|
20
|
+
|
|
21
|
+
if (!query.trim()) {
|
|
22
|
+
model.set("query_error", "Please enter a query");
|
|
23
|
+
model.save_changes();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (backend === "neo4j") {
|
|
28
|
+
const result = await neo4jBackend.executeQuery(
|
|
29
|
+
query,
|
|
30
|
+
model.get("connection_database"),
|
|
31
|
+
model
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (result) {
|
|
35
|
+
model.set("nodes", result.nodes);
|
|
36
|
+
model.set("edges", result.edges);
|
|
37
|
+
model.save_changes();
|
|
38
|
+
}
|
|
39
|
+
} else if (backend === "grafeo") {
|
|
40
|
+
// Trigger Python-side execution
|
|
41
|
+
model.set("_execute_query", model.get("_execute_query") + 1);
|
|
42
|
+
model.save_changes();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Main render function for the anywidget.
|
|
48
|
+
*/
|
|
49
|
+
function render({ model, el }) {
|
|
50
|
+
const wrapper = document.createElement("div");
|
|
51
|
+
wrapper.className = "awg-wrapper";
|
|
52
|
+
|
|
53
|
+
// Apply dark mode
|
|
54
|
+
function updateTheme() {
|
|
55
|
+
wrapper.classList.toggle("awg-dark", model.get("dark_mode"));
|
|
56
|
+
}
|
|
57
|
+
updateTheme();
|
|
58
|
+
model.on("change:dark_mode", updateTheme);
|
|
59
|
+
|
|
60
|
+
// Create query executor callback
|
|
61
|
+
const onExecuteQuery = () => executeQuery(model);
|
|
62
|
+
|
|
63
|
+
// Create toolbar if enabled
|
|
64
|
+
if (model.get("show_toolbar")) {
|
|
65
|
+
const toolbar = createToolbar(model, wrapper, onExecuteQuery);
|
|
66
|
+
wrapper.appendChild(toolbar);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Main content area (schema + graph + properties)
|
|
70
|
+
const content = document.createElement("div");
|
|
71
|
+
content.className = "awg-content";
|
|
72
|
+
|
|
73
|
+
// Create schema sidebar (left) - collapsed by default
|
|
74
|
+
const schemaSidebar = createSchemaPanel(model, onExecuteQuery);
|
|
75
|
+
content.appendChild(schemaSidebar);
|
|
76
|
+
|
|
77
|
+
// Create graph container
|
|
78
|
+
const container = document.createElement("div");
|
|
79
|
+
container.className = "awg-graph-container";
|
|
80
|
+
container.style.width = model.get("width") + "px";
|
|
81
|
+
container.style.height = model.get("height") + "px";
|
|
82
|
+
content.appendChild(container);
|
|
83
|
+
|
|
84
|
+
// Create properties panel (right) - collapsed by default
|
|
85
|
+
const propertiesPanel = createPropertiesPanel(model);
|
|
86
|
+
content.appendChild(propertiesPanel);
|
|
87
|
+
|
|
88
|
+
// Create settings panel if enabled
|
|
89
|
+
if (model.get("show_settings")) {
|
|
90
|
+
const settingsPanel = createSettingsPanel(model);
|
|
91
|
+
content.appendChild(settingsPanel);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
wrapper.appendChild(content);
|
|
95
|
+
el.appendChild(wrapper);
|
|
96
|
+
|
|
97
|
+
// Initialize Graphology graph
|
|
98
|
+
const graph = new Graph();
|
|
99
|
+
|
|
100
|
+
// Add initial nodes
|
|
101
|
+
const nodes = model.get("nodes") || [];
|
|
102
|
+
nodes.forEach((node) => {
|
|
103
|
+
graph.addNode(node.id, {
|
|
104
|
+
label: node.label || node.id,
|
|
105
|
+
x: node.x ?? Math.random() * 100,
|
|
106
|
+
y: node.y ?? Math.random() * 100,
|
|
107
|
+
size: node.size || 10,
|
|
108
|
+
color: node.color || "#6366f1",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Add initial edges
|
|
113
|
+
const edges = model.get("edges") || [];
|
|
114
|
+
edges.forEach((edge) => {
|
|
115
|
+
graph.addEdge(edge.source, edge.target, {
|
|
116
|
+
label: edge.label || "",
|
|
117
|
+
size: edge.size || 2,
|
|
118
|
+
color: edge.color || "#94a3b8",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Initialize Sigma renderer
|
|
123
|
+
const renderer = new Sigma(graph, container, {
|
|
124
|
+
renderLabels: model.get("show_labels"),
|
|
125
|
+
renderEdgeLabels: model.get("show_edge_labels"),
|
|
126
|
+
defaultNodeColor: "#6366f1",
|
|
127
|
+
defaultEdgeColor: "#94a3b8",
|
|
128
|
+
labelColor: { color: "#333" },
|
|
129
|
+
labelSize: 12,
|
|
130
|
+
labelWeight: "500",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Node click handler
|
|
134
|
+
renderer.on("clickNode", ({ node }) => {
|
|
135
|
+
const nodeData = graph.getNodeAttributes(node);
|
|
136
|
+
model.set("selected_node", { id: node, ...nodeData });
|
|
137
|
+
model.save_changes();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Edge click handler
|
|
141
|
+
renderer.on("clickEdge", ({ edge }) => {
|
|
142
|
+
const edgeData = graph.getEdgeAttributes(edge);
|
|
143
|
+
const [source, target] = graph.extremities(edge);
|
|
144
|
+
model.set("selected_edge", { source, target, ...edgeData });
|
|
145
|
+
model.save_changes();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Stage click (deselect)
|
|
149
|
+
renderer.on("clickStage", () => {
|
|
150
|
+
model.set("selected_node", null);
|
|
151
|
+
model.set("selected_edge", null);
|
|
152
|
+
model.save_changes();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Update graph when nodes change
|
|
156
|
+
model.on("change:nodes", () => {
|
|
157
|
+
graph.clear();
|
|
158
|
+
const newNodes = model.get("nodes") || [];
|
|
159
|
+
newNodes.forEach((node) => {
|
|
160
|
+
graph.addNode(node.id, {
|
|
161
|
+
label: node.label || node.id,
|
|
162
|
+
x: node.x ?? Math.random() * 100,
|
|
163
|
+
y: node.y ?? Math.random() * 100,
|
|
164
|
+
size: node.size || 10,
|
|
165
|
+
color: node.color || "#6366f1",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
const newEdges = model.get("edges") || [];
|
|
169
|
+
newEdges.forEach((edge) => {
|
|
170
|
+
graph.addEdge(edge.source, edge.target, {
|
|
171
|
+
label: edge.label || "",
|
|
172
|
+
size: edge.size || 2,
|
|
173
|
+
color: edge.color || "#94a3b8",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
renderer.refresh();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update graph when edges change
|
|
180
|
+
model.on("change:edges", () => {
|
|
181
|
+
graph.clearEdges();
|
|
182
|
+
const newEdges = model.get("edges") || [];
|
|
183
|
+
newEdges.forEach((edge) => {
|
|
184
|
+
graph.addEdge(edge.source, edge.target, {
|
|
185
|
+
label: edge.label || "",
|
|
186
|
+
size: edge.size || 2,
|
|
187
|
+
color: edge.color || "#94a3b8",
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
renderer.refresh();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Cleanup on destroy
|
|
194
|
+
return () => {
|
|
195
|
+
renderer.kill();
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default { render };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neo4j database connection and query execution (browser-side).
|
|
3
|
+
*/
|
|
4
|
+
import neo4j from "https://cdn.jsdelivr.net/npm/neo4j-driver@5.28.0/lib/browser/neo4j-web.esm.min.js";
|
|
5
|
+
|
|
6
|
+
let driver = null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Connect to a Neo4j database.
|
|
10
|
+
*/
|
|
11
|
+
export async function connect(uri, username, password, model) {
|
|
12
|
+
if (driver) {
|
|
13
|
+
await driver.close();
|
|
14
|
+
driver = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model.set("connection_status", "connecting");
|
|
18
|
+
model.save_changes();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
driver = neo4j.driver(uri, neo4j.auth.basic(username, password));
|
|
22
|
+
await driver.verifyConnectivity();
|
|
23
|
+
model.set("connection_status", "connected");
|
|
24
|
+
model.set("query_error", "");
|
|
25
|
+
model.save_changes();
|
|
26
|
+
|
|
27
|
+
await fetchSchema(model);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
model.set("connection_status", "error");
|
|
31
|
+
model.set("query_error", "Connection failed: " + error.message);
|
|
32
|
+
model.save_changes();
|
|
33
|
+
driver = null;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Disconnect from Neo4j database.
|
|
40
|
+
*/
|
|
41
|
+
export async function disconnect(model) {
|
|
42
|
+
if (driver) {
|
|
43
|
+
await driver.close();
|
|
44
|
+
driver = null;
|
|
45
|
+
}
|
|
46
|
+
model.set("connection_status", "disconnected");
|
|
47
|
+
model.save_changes();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if connected to Neo4j.
|
|
52
|
+
*/
|
|
53
|
+
export function isConnected() {
|
|
54
|
+
return driver !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetch database schema (labels and relationship types).
|
|
59
|
+
*/
|
|
60
|
+
export async function fetchSchema(model) {
|
|
61
|
+
if (!driver) return;
|
|
62
|
+
|
|
63
|
+
const database = model.get("connection_database") || "neo4j";
|
|
64
|
+
const session = driver.session({ database });
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Fetch node labels and their properties
|
|
68
|
+
const labelsResult = await session.run("CALL db.labels()");
|
|
69
|
+
const nodeTypes = [];
|
|
70
|
+
|
|
71
|
+
for (const record of labelsResult.records) {
|
|
72
|
+
const label = record.get(0);
|
|
73
|
+
const propsResult = await session.run(
|
|
74
|
+
`MATCH (n:\`${label}\`) UNWIND keys(n) AS key RETURN DISTINCT key LIMIT 20`
|
|
75
|
+
);
|
|
76
|
+
const properties = propsResult.records.map((r) => r.get(0));
|
|
77
|
+
nodeTypes.push({ label, properties, count: null });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fetch relationship types and their properties
|
|
81
|
+
const relTypesResult = await session.run("CALL db.relationshipTypes()");
|
|
82
|
+
const edgeTypes = [];
|
|
83
|
+
|
|
84
|
+
for (const record of relTypesResult.records) {
|
|
85
|
+
const type = record.get(0);
|
|
86
|
+
const propsResult = await session.run(
|
|
87
|
+
`MATCH ()-[r:\`${type}\`]->() UNWIND keys(r) AS key RETURN DISTINCT key LIMIT 20`
|
|
88
|
+
);
|
|
89
|
+
const properties = propsResult.records.map((r) => r.get(0));
|
|
90
|
+
edgeTypes.push({ type, properties, count: null });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
model.set("schema_node_types", nodeTypes);
|
|
94
|
+
model.set("schema_edge_types", edgeTypes);
|
|
95
|
+
model.save_changes();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error("Failed to fetch schema:", error);
|
|
98
|
+
} finally {
|
|
99
|
+
await session.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute a Cypher query.
|
|
105
|
+
*/
|
|
106
|
+
export async function executeQuery(query, database, model) {
|
|
107
|
+
if (!driver) {
|
|
108
|
+
model.set("query_error", "Not connected to database");
|
|
109
|
+
model.save_changes();
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
model.set("query_running", true);
|
|
114
|
+
model.set("query_error", "");
|
|
115
|
+
model.save_changes();
|
|
116
|
+
|
|
117
|
+
const session = driver.session({ database: database || "neo4j" });
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await session.run(query);
|
|
121
|
+
model.set("query_running", false);
|
|
122
|
+
model.save_changes();
|
|
123
|
+
return processRecords(result.records);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
model.set("query_running", false);
|
|
126
|
+
model.set("query_error", "Query error: " + error.message);
|
|
127
|
+
model.save_changes();
|
|
128
|
+
return null;
|
|
129
|
+
} finally {
|
|
130
|
+
await session.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Process Neo4j records into nodes and edges.
|
|
136
|
+
*/
|
|
137
|
+
function processRecords(records) {
|
|
138
|
+
const nodes = new Map();
|
|
139
|
+
const edges = [];
|
|
140
|
+
|
|
141
|
+
for (const record of records) {
|
|
142
|
+
for (const key of record.keys) {
|
|
143
|
+
const value = record.get(key);
|
|
144
|
+
processValue(value, nodes, edges);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { nodes: Array.from(nodes.values()), edges };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Process a single Neo4j value (node, relationship, path, or array).
|
|
153
|
+
*/
|
|
154
|
+
function processValue(value, nodes, edges) {
|
|
155
|
+
if (!value) return;
|
|
156
|
+
|
|
157
|
+
if (neo4j.isNode(value)) {
|
|
158
|
+
const nodeId = value.elementId || value.identity.toString();
|
|
159
|
+
if (!nodes.has(nodeId)) {
|
|
160
|
+
const props = {};
|
|
161
|
+
for (const [k, v] of Object.entries(value.properties)) {
|
|
162
|
+
props[k] = neo4j.isInt(v) ? v.toNumber() : v;
|
|
163
|
+
}
|
|
164
|
+
nodes.set(nodeId, {
|
|
165
|
+
id: nodeId,
|
|
166
|
+
label: props.name || props.title || value.labels[0] || nodeId,
|
|
167
|
+
labels: value.labels,
|
|
168
|
+
...props,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
} else if (neo4j.isRelationship(value)) {
|
|
172
|
+
const props = {};
|
|
173
|
+
for (const [k, v] of Object.entries(value.properties)) {
|
|
174
|
+
props[k] = neo4j.isInt(v) ? v.toNumber() : v;
|
|
175
|
+
}
|
|
176
|
+
edges.push({
|
|
177
|
+
source: value.startNodeElementId || value.start.toString(),
|
|
178
|
+
target: value.endNodeElementId || value.end.toString(),
|
|
179
|
+
label: value.type,
|
|
180
|
+
...props,
|
|
181
|
+
});
|
|
182
|
+
} else if (neo4j.isPath(value)) {
|
|
183
|
+
for (const segment of value.segments) {
|
|
184
|
+
processValue(segment.start, nodes, edges);
|
|
185
|
+
processValue(segment.end, nodes, edges);
|
|
186
|
+
processValue(segment.relationship, nodes, edges);
|
|
187
|
+
}
|
|
188
|
+
} else if (Array.isArray(value)) {
|
|
189
|
+
for (const item of value) {
|
|
190
|
+
processValue(item, nodes, edges);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|