184 lines
4.0 KiB
Go
184 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand/v2"
|
|
|
|
ops "fn-registry/fn_operations"
|
|
)
|
|
|
|
// buildGraphData converts entities and relations from operations.db into sigma.js-compatible GraphData.
|
|
func buildGraphData(db *ops.DB) (GraphData, error) {
|
|
entities, err := db.ListEntities("", "")
|
|
if err != nil {
|
|
return GraphData{}, fmt.Errorf("listing entities: %w", err)
|
|
}
|
|
|
|
relations, err := db.ListRelations("")
|
|
if err != nil {
|
|
return GraphData{}, fmt.Errorf("listing relations: %w", err)
|
|
}
|
|
|
|
// Compute degree per node
|
|
degree := map[string]int{}
|
|
for _, r := range relations {
|
|
degree[r.FromEntity]++
|
|
degree[r.ToEntity]++
|
|
}
|
|
|
|
// Build nodes
|
|
nodes := make([]GraphNode, 0, len(entities))
|
|
for _, e := range entities {
|
|
color, ok := entityTypeColors[e.TypeRef]
|
|
if !ok {
|
|
color = "#95a5a6" // default grey
|
|
}
|
|
|
|
// Size: base + degree factor + risk_score factor
|
|
size := 8.0
|
|
size += math.Min(float64(degree[e.ID])*2.0, 20.0)
|
|
if rs, ok := e.Metadata["risk_score"]; ok {
|
|
if v, ok := toFloat64(rs); ok {
|
|
size += v * 0.1 // risk_score contributes up to ~10
|
|
}
|
|
}
|
|
|
|
nodes = append(nodes, GraphNode{
|
|
ID: e.ID,
|
|
Label: e.Name,
|
|
Type: e.TypeRef,
|
|
Color: color,
|
|
Size: size,
|
|
X: rand.Float64()*100 - 50,
|
|
Y: rand.Float64()*100 - 50,
|
|
Extra: e.Metadata,
|
|
})
|
|
}
|
|
|
|
// Build edges
|
|
edges := make([]GraphEdge, 0, len(relations))
|
|
for _, r := range relations {
|
|
w := 1.0
|
|
if r.Weight != nil {
|
|
w = *r.Weight
|
|
}
|
|
|
|
edges = append(edges, GraphEdge{
|
|
ID: r.ID,
|
|
Source: r.FromEntity,
|
|
Target: r.ToEntity,
|
|
Label: r.Name,
|
|
Color: "#ffffff30",
|
|
Size: math.Max(w*3, 0.5),
|
|
Type: "arrow",
|
|
})
|
|
}
|
|
|
|
return GraphData{Nodes: nodes, Edges: edges}, nil
|
|
}
|
|
|
|
// buildEgoGraph returns a subgraph centered on entityID up to depth hops.
|
|
func buildEgoGraph(db *ops.DB, entityID string, depth int) (GraphData, error) {
|
|
if depth < 1 {
|
|
depth = 1
|
|
}
|
|
if depth > 5 {
|
|
depth = 5
|
|
}
|
|
|
|
relations, err := db.ListRelations("")
|
|
if err != nil {
|
|
return GraphData{}, err
|
|
}
|
|
|
|
// BFS to collect entity IDs within depth
|
|
visited := map[string]bool{entityID: true}
|
|
frontier := []string{entityID}
|
|
for d := 0; d < depth; d++ {
|
|
var next []string
|
|
for _, id := range frontier {
|
|
for _, r := range relations {
|
|
if r.FromEntity == id && !visited[r.ToEntity] {
|
|
visited[r.ToEntity] = true
|
|
next = append(next, r.ToEntity)
|
|
}
|
|
if r.ToEntity == id && !visited[r.FromEntity] {
|
|
visited[r.FromEntity] = true
|
|
next = append(next, r.FromEntity)
|
|
}
|
|
}
|
|
}
|
|
frontier = next
|
|
}
|
|
|
|
// Filter entities and relations to subgraph
|
|
entities, err := db.ListEntities("", "")
|
|
if err != nil {
|
|
return GraphData{}, err
|
|
}
|
|
|
|
degree := map[string]int{}
|
|
var subRels []ops.Relation
|
|
for _, r := range relations {
|
|
if visited[r.FromEntity] && visited[r.ToEntity] {
|
|
subRels = append(subRels, r)
|
|
degree[r.FromEntity]++
|
|
degree[r.ToEntity]++
|
|
}
|
|
}
|
|
|
|
nodes := make([]GraphNode, 0)
|
|
for _, e := range entities {
|
|
if !visited[e.ID] {
|
|
continue
|
|
}
|
|
color, ok := entityTypeColors[e.TypeRef]
|
|
if !ok {
|
|
color = "#95a5a6"
|
|
}
|
|
size := 8.0 + math.Min(float64(degree[e.ID])*2.0, 20.0)
|
|
if rs, ok := e.Metadata["risk_score"]; ok {
|
|
if v, ok := toFloat64(rs); ok {
|
|
size += v * 0.1
|
|
}
|
|
}
|
|
nodes = append(nodes, GraphNode{
|
|
ID: e.ID, Label: e.Name, Type: e.TypeRef,
|
|
Color: color, Size: size,
|
|
X: rand.Float64()*100 - 50, Y: rand.Float64()*100 - 50,
|
|
Extra: e.Metadata,
|
|
})
|
|
}
|
|
|
|
edges := make([]GraphEdge, 0)
|
|
for _, r := range subRels {
|
|
w := 1.0
|
|
if r.Weight != nil {
|
|
w = *r.Weight
|
|
}
|
|
edges = append(edges, GraphEdge{
|
|
ID: r.ID, Source: r.FromEntity, Target: r.ToEntity,
|
|
Label: r.Name, Color: "#ffffff30",
|
|
Size: math.Max(w*3, 0.5), Type: "arrow",
|
|
})
|
|
}
|
|
|
|
return GraphData{Nodes: nodes, Edges: edges}, nil
|
|
}
|
|
|
|
func toFloat64(v any) (float64, bool) {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return n, true
|
|
case float32:
|
|
return float64(n), true
|
|
case int:
|
|
return float64(n), true
|
|
case int64:
|
|
return float64(n), true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|