Skip to main content

velesdb_server/handlers/graph/
types.rs

1//! Graph types for VelesDB REST API.
2//!
3//! Contains request/response types for graph operations.
4
5use serde::{Deserialize, Serialize};
6use utoipa::{IntoParams, ToSchema};
7use velesdb_core::api_types::serde_id;
8
9/// A single traversal result item.
10#[derive(Debug, Clone, Serialize, ToSchema)]
11pub struct TraversalResultItem {
12    /// Target node ID reached.
13    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
14    #[cfg_attr(feature = "openapi", schema(value_type = String))]
15    pub target_id: u64,
16    /// Depth of traversal (number of hops from source).
17    pub depth: u32,
18    /// Path taken (list of edge IDs).
19    #[serde(serialize_with = "serde_id::serialize_ids_as_strings")]
20    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
21    pub path: Vec<u64>,
22}
23
24/// Query parameters for edge operations.
25#[derive(Debug, Deserialize, IntoParams)]
26pub struct EdgeQueryParams {
27    /// Filter edges by label (e.g., "KNOWS", "FOLLOWS").
28    #[param(example = "KNOWS")]
29    pub label: Option<String>,
30}
31
32/// Request for graph traversal.
33#[derive(Debug, Deserialize, ToSchema)]
34pub struct TraverseRequest {
35    /// Source node ID to start traversal from.
36    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
37    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::id_input_schema))]
38    pub source: u64,
39    /// Traversal strategy: "bfs" or "dfs".
40    #[serde(default = "default_strategy")]
41    pub strategy: String,
42    /// Maximum traversal depth.
43    #[serde(default = "default_max_depth")]
44    pub max_depth: u32,
45    /// Maximum number of results to return.
46    #[serde(default = "default_limit")]
47    pub limit: usize,
48    /// Filter by relationship types (empty = all types).
49    #[serde(default)]
50    pub rel_types: Vec<String>,
51}
52
53fn default_strategy() -> String {
54    "bfs".to_string()
55}
56
57fn default_max_depth() -> u32 {
58    3
59}
60
61fn default_limit() -> usize {
62    100
63}
64
65/// Response from graph traversal.
66#[derive(Debug, Serialize, ToSchema)]
67pub struct TraverseResponse {
68    /// List of traversal results.
69    pub results: Vec<TraversalResultItem>,
70    /// Whether more results are available.
71    pub has_more: bool,
72    /// Traversal statistics.
73    pub stats: TraversalStats,
74}
75
76/// Statistics from traversal operation.
77#[derive(Debug, Serialize, ToSchema)]
78pub struct TraversalStats {
79    /// Number of nodes visited.
80    pub visited: usize,
81    /// Maximum depth reached.
82    pub depth_reached: u32,
83}
84
85/// Response for node degree query.
86#[derive(Debug, Serialize, ToSchema)]
87pub struct DegreeResponse {
88    /// Number of incoming edges.
89    pub in_degree: usize,
90    /// Number of outgoing edges.
91    pub out_degree: usize,
92}
93
94/// Response containing edges.
95#[derive(Debug, Serialize, ToSchema)]
96pub struct EdgesResponse {
97    /// List of edges.
98    pub edges: Vec<EdgeResponse>,
99    /// Total count of edges returned.
100    pub count: usize,
101}
102
103/// A single edge in the response.
104#[derive(Debug, Serialize, ToSchema)]
105pub struct EdgeResponse {
106    /// Edge ID.
107    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
108    #[cfg_attr(feature = "openapi", schema(value_type = String))]
109    pub id: u64,
110    /// Source node ID.
111    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
112    #[cfg_attr(feature = "openapi", schema(value_type = String))]
113    pub source: u64,
114    /// Target node ID.
115    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
116    #[cfg_attr(feature = "openapi", schema(value_type = String))]
117    pub target: u64,
118    /// Edge label (relationship type).
119    pub label: String,
120    /// Edge properties.
121    pub properties: serde_json::Value,
122}
123
124/// Request to add an edge to the graph.
125#[derive(Debug, Deserialize, ToSchema)]
126pub struct AddEdgeRequest {
127    /// Edge ID.
128    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
129    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::id_input_schema))]
130    pub id: u64,
131    /// Source node ID.
132    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
133    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::id_input_schema))]
134    pub source: u64,
135    /// Target node ID.
136    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
137    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::id_input_schema))]
138    pub target: u64,
139    /// Edge label (relationship type).
140    pub label: String,
141    /// Edge properties.
142    #[serde(default)]
143    pub properties: serde_json::Value,
144}
145
146// ============================================================================
147// Edge Count, Node List, Node Payload, Parallel Traversal, Graph Search
148// ============================================================================
149
150/// Response for edge count query.
151#[derive(Debug, Serialize, ToSchema)]
152pub struct EdgeCountResponse {
153    /// Total number of edges in the graph.
154    pub count: usize,
155}
156
157/// Query parameters for node-scoped edge queries.
158#[derive(Debug, Deserialize, IntoParams)]
159pub struct NodeEdgeQueryParams {
160    /// Filter by direction: "in", "out", or "both".
161    #[serde(default = "default_direction")]
162    #[param(example = "out")]
163    pub direction: String,
164    /// Filter edges by label.
165    #[param(example = "KNOWS")]
166    pub label: Option<String>,
167}
168
169fn default_direction() -> String {
170    "out".to_string()
171}
172
173/// Response containing all node IDs in the graph.
174#[derive(Debug, Serialize, ToSchema)]
175pub struct NodeListResponse {
176    /// List of node IDs (serialized as strings to preserve u64 precision in JS).
177    #[serde(serialize_with = "serde_id::serialize_ids_as_strings")]
178    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
179    pub node_ids: Vec<u64>,
180    /// Total count of nodes.
181    pub count: usize,
182}
183
184/// Request to upsert a node payload.
185#[derive(Debug, Deserialize, ToSchema)]
186pub struct UpsertNodePayloadRequest {
187    /// JSON payload to store on the node.
188    pub payload: serde_json::Value,
189}
190
191/// Response for a node payload retrieval.
192#[derive(Debug, Serialize, ToSchema)]
193pub struct NodePayloadResponse {
194    /// Node ID.
195    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
196    #[cfg_attr(feature = "openapi", schema(value_type = String))]
197    pub node_id: u64,
198    /// Stored payload (null if none).
199    pub payload: Option<serde_json::Value>,
200}
201
202/// Request for parallel multi-source BFS traversal.
203#[derive(Debug, Deserialize, ToSchema)]
204pub struct ParallelTraverseRequest {
205    /// Source node IDs to start traversal from (accepts strings or numbers so
206    /// JS clients can send precision-safe u64 IDs above `Number.MAX_SAFE_INTEGER`).
207    #[serde(deserialize_with = "serde_id::deserialize_ids_from_string_or_number")]
208    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
209    pub sources: Vec<u64>,
210    /// Maximum traversal depth.
211    #[serde(default = "default_max_depth")]
212    pub max_depth: u32,
213    /// Maximum number of results per source.
214    #[serde(default = "default_limit")]
215    pub limit: usize,
216    /// Filter by relationship types (empty = all types).
217    #[serde(default)]
218    pub rel_types: Vec<String>,
219}
220
221/// Request for graph embedding search.
222#[derive(Debug, Deserialize, ToSchema)]
223pub struct GraphSearchRequest {
224    /// Query vector for similarity search.
225    pub vector: Vec<f32>,
226    /// Number of results to return.
227    #[serde(default = "default_graph_search_k")]
228    pub top_k: usize,
229}
230
231fn default_graph_search_k() -> usize {
232    10
233}
234
235/// Response for graph embedding search.
236#[derive(Debug, Serialize, ToSchema)]
237pub struct GraphSearchResponse {
238    /// Search results with node ID and similarity score.
239    pub results: Vec<GraphSearchResultItem>,
240}
241
242/// A single graph search result.
243#[derive(Debug, Serialize, ToSchema)]
244pub struct GraphSearchResultItem {
245    /// Node ID.
246    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
247    #[cfg_attr(feature = "openapi", schema(value_type = String))]
248    pub id: u64,
249    /// Similarity score.
250    pub score: f32,
251    /// Node payload (if any).
252    pub payload: Option<serde_json::Value>,
253}
254
255// ============================================================================
256// SSE Streaming Types (EPIC-058 US-003)
257// ============================================================================
258
259/// Query parameters for streaming graph traversal.
260#[derive(Debug, Deserialize, IntoParams)]
261pub struct StreamTraverseParams {
262    /// Source node ID to start traversal from.
263    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
264    #[param(example = 123)]
265    pub start_node: u64,
266    /// Traversal algorithm: "bfs" or "dfs".
267    #[serde(default = "default_algorithm")]
268    #[param(example = "bfs")]
269    pub algorithm: String,
270    /// Maximum traversal depth.
271    #[serde(default = "default_stream_max_depth")]
272    #[param(example = 5)]
273    pub max_depth: u32,
274    /// Maximum number of results to stream.
275    #[serde(default = "default_stream_limit")]
276    #[param(example = 1000)]
277    pub limit: usize,
278    /// Filter by relationship types (comma-separated).
279    #[serde(default)]
280    #[param(example = "KNOWS,FOLLOWS")]
281    pub relationship_types: Option<String>,
282}
283
284fn default_algorithm() -> String {
285    "bfs".to_string()
286}
287
288fn default_stream_max_depth() -> u32 {
289    5
290}
291
292fn default_stream_limit() -> usize {
293    1000
294}
295
296/// SSE event: A node reached during traversal.
297#[derive(Debug, Serialize, ToSchema)]
298pub struct StreamNodeEvent {
299    /// Target node ID.
300    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
301    #[cfg_attr(feature = "openapi", schema(value_type = String))]
302    pub id: u64,
303    /// Depth from source.
304    pub depth: u32,
305    /// Path of edge IDs taken to reach this node.
306    #[serde(serialize_with = "serde_id::serialize_ids_as_strings")]
307    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
308    pub path: Vec<u64>,
309}
310
311/// SSE event: Periodic statistics update.
312#[derive(Debug, Serialize, ToSchema)]
313pub struct StreamStatsEvent {
314    /// Number of nodes visited so far.
315    pub nodes_visited: usize,
316    /// Elapsed time in milliseconds.
317    pub elapsed_ms: u64,
318}
319
320/// SSE event: Traversal completed.
321#[derive(Debug, Serialize, ToSchema)]
322pub struct StreamDoneEvent {
323    /// Total nodes returned.
324    pub total_nodes: usize,
325    /// Maximum depth reached.
326    pub max_depth_reached: u32,
327    /// Total elapsed time in milliseconds.
328    pub elapsed_ms: u64,
329}
330
331/// SSE event: Error occurred.
332#[derive(Debug, Serialize, ToSchema)]
333pub struct StreamErrorEvent {
334    /// Error message.
335    pub error: String,
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    /// Path edge IDs above 2^53 must serialize as a JSON string array.
343    #[test]
344    fn test_traversal_path_serialized_as_strings() {
345        let above_safe = (1_u64 << 53) + 1; // 9_007_199_254_740_993
346        let item = TraversalResultItem {
347            target_id: 2,
348            depth: 1,
349            path: vec![above_safe],
350        };
351        let json = serde_json::to_value(&item).unwrap();
352        assert_eq!(json["path"], serde_json::json!(["9007199254740993"]));
353    }
354
355    /// Streamed node path edge IDs above 2^53 must serialize as strings.
356    #[test]
357    fn test_stream_node_event_path_serialized_as_strings() {
358        let above_safe = (1_u64 << 53) + 1;
359        let event = StreamNodeEvent {
360            id: 1,
361            depth: 1,
362            path: vec![above_safe],
363        };
364        let json = serde_json::to_value(&event).unwrap();
365        assert_eq!(json["path"], serde_json::json!(["9007199254740993"]));
366    }
367
368    /// Node-list ids above 2^53 must serialize as a JSON string array.
369    #[test]
370    fn test_node_list_ids_serialized_as_strings() {
371        let above_safe = (1_u64 << 53) + 1;
372        let response = NodeListResponse {
373            node_ids: vec![1, above_safe],
374            count: 2,
375        };
376        let json = serde_json::to_value(&response).unwrap();
377        assert_eq!(
378            json["node_ids"],
379            serde_json::json!(["1", "9007199254740993"])
380        );
381    }
382
383    /// Parallel-traverse sources must deserialize from BOTH strings and numbers.
384    #[test]
385    fn test_parallel_sources_accepts_strings_and_numbers() {
386        let from_strings: ParallelTraverseRequest =
387            serde_json::from_value(serde_json::json!({ "sources": ["9007199254740993", "2"] }))
388                .expect("string sources must deserialize");
389        assert_eq!(from_strings.sources, vec![(1_u64 << 53) + 1, 2]);
390
391        let from_numbers: ParallelTraverseRequest =
392            serde_json::from_value(serde_json::json!({ "sources": [3, 4] }))
393                .expect("numeric sources must still deserialize");
394        assert_eq!(from_numbers.sources, vec![3, 4]);
395    }
396}