1use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7pub type Props = BTreeMap<String, serde_json::Value>;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct NodeMetadata {
13 pub created_at: String,
14 pub updated_at: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub source_type: Option<String>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub source_id: Option<String>,
19 pub realm: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct GraphNode {
24 pub id: String,
25 pub labels: Vec<String>,
26 #[serde(default)]
27 pub properties: Props,
28 pub metadata: NodeMetadata,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct GraphRelationship {
33 pub id: String,
34 pub source_id: String,
35 pub target_id: String,
36 pub relationship_type: String,
37 #[serde(default)]
38 pub properties: Props,
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
42pub struct GraphStats {
43 pub total_nodes: usize,
44 pub total_relationships: usize,
45 pub label_counts: BTreeMap<String, usize>,
46 pub relationship_type_counts: BTreeMap<String, usize>,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct GraphPayload {
51 pub stats: GraphStats,
52 pub nodes: Vec<GraphNode>,
53 pub edges: Vec<GraphRelationship>,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct EdgeExpansion {
58 pub edges: Vec<GraphRelationship>,
59 pub new_node_ids: Vec<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct SearchHits {
64 pub hits: Vec<GraphNode>,
65 pub total: usize,
66 pub limit: usize,
67 pub offset: usize,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct GraphSchema {
72 pub node_kinds: Vec<String>,
73 pub edge_types: Vec<String>,
74 pub property_keys: BTreeMap<String, Vec<String>>,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct GraphRef {
80 pub name: String,
81 pub kind: String,
83 pub description: String,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "lowercase")]
88pub enum Direction {
89 Forward,
90 Backward,
91 Both,
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn graph_node_serializes_to_wire_shape() {
100 let node = GraphNode {
101 id: "default::otel_logs".into(),
102 labels: vec!["Table".into()],
103 properties: [("database".to_string(), serde_json::json!("default"))]
104 .into_iter()
105 .collect(),
106 metadata: NodeMetadata {
107 created_at: "2026-05-25T00:00:00Z".into(),
108 updated_at: "2026-05-25T00:00:00Z".into(),
109 source_type: Some("schema".into()),
110 source_id: None,
111 realm: "default".into(),
112 },
113 };
114 let v = serde_json::to_value(&node).unwrap();
115 assert_eq!(v["id"], "default::otel_logs");
116 assert_eq!(v["labels"][0], "Table");
117 assert_eq!(v["properties"]["database"], "default");
118 assert_eq!(v["metadata"]["realm"], "default");
119 assert_eq!(v["metadata"]["source_type"], "schema");
120 assert!(v["metadata"].get("source_id").is_none());
121 }
122
123 #[test]
124 fn direction_serializes_lowercase() {
125 assert_eq!(serde_json::to_value(Direction::Forward).unwrap(), "forward");
126 assert_eq!(serde_json::to_value(Direction::Both).unwrap(), "both");
127 }
128}