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/// Request to add multiple edges in one batched operation.
147#[derive(Debug, Deserialize, ToSchema)]
148pub struct AddEdgesBatchRequest {
149    /// Edges to insert.
150    pub edges: Vec<AddEdgeRequest>,
151}
152
153/// Response for a batched edge insertion.
154#[derive(Debug, Serialize, ToSchema)]
155pub struct AddEdgesBatchResponse {
156    /// Number of edges actually inserted. May be less than the number submitted:
157    /// edges whose ID already exists (in the request or the graph) are silently
158    /// skipped. Compare against the request length to detect skipped duplicates.
159    pub added: usize,
160}
161
162// ============================================================================
163// Edge Count, Node List, Node Payload, Parallel Traversal, Graph Search
164// ============================================================================
165
166/// Response for edge count query.
167#[derive(Debug, Serialize, ToSchema)]
168pub struct EdgeCountResponse {
169    /// Total number of edges in the graph.
170    pub count: usize,
171}
172
173/// Query parameters for node-scoped edge queries.
174#[derive(Debug, Deserialize, IntoParams)]
175pub struct NodeEdgeQueryParams {
176    /// Filter by direction: "in", "out", or "both".
177    #[serde(default = "default_direction")]
178    #[param(example = "out")]
179    pub direction: String,
180    /// Filter edges by label.
181    #[param(example = "KNOWS")]
182    pub label: Option<String>,
183}
184
185fn default_direction() -> String {
186    "out".to_string()
187}
188
189/// Response containing all node IDs in the graph.
190#[derive(Debug, Serialize, ToSchema)]
191pub struct NodeListResponse {
192    /// List of node IDs (serialized as strings to preserve u64 precision in JS).
193    #[serde(serialize_with = "serde_id::serialize_ids_as_strings")]
194    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
195    pub node_ids: Vec<u64>,
196    /// Total count of nodes.
197    pub count: usize,
198}
199
200/// Request to upsert a node payload.
201#[derive(Debug, Deserialize, ToSchema)]
202pub struct UpsertNodePayloadRequest {
203    /// JSON payload to store on the node.
204    pub payload: serde_json::Value,
205}
206
207/// Response for a node payload retrieval.
208#[derive(Debug, Serialize, ToSchema)]
209pub struct NodePayloadResponse {
210    /// Node ID.
211    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
212    #[cfg_attr(feature = "openapi", schema(value_type = String))]
213    pub node_id: u64,
214    /// Stored payload (null if none).
215    pub payload: Option<serde_json::Value>,
216}
217
218/// Request for parallel multi-source BFS traversal.
219#[derive(Debug, Deserialize, ToSchema)]
220pub struct ParallelTraverseRequest {
221    /// Source node IDs to start traversal from (accepts strings or numbers so
222    /// JS clients can send precision-safe u64 IDs above `Number.MAX_SAFE_INTEGER`).
223    #[serde(deserialize_with = "serde_id::deserialize_ids_from_string_or_number")]
224    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
225    pub sources: Vec<u64>,
226    /// Maximum traversal depth.
227    #[serde(default = "default_max_depth")]
228    pub max_depth: u32,
229    /// Maximum number of results per source.
230    #[serde(default = "default_limit")]
231    pub limit: usize,
232    /// Filter by relationship types (empty = all types).
233    #[serde(default)]
234    pub rel_types: Vec<String>,
235}
236
237/// Request for graph embedding search.
238#[derive(Debug, Deserialize, ToSchema)]
239pub struct GraphSearchRequest {
240    /// Query vector for similarity search.
241    pub vector: Vec<f32>,
242    /// Number of results to return.
243    #[serde(default = "default_graph_search_k")]
244    pub top_k: usize,
245}
246
247fn default_graph_search_k() -> usize {
248    10
249}
250
251/// Response for graph embedding search.
252#[derive(Debug, Serialize, ToSchema)]
253pub struct GraphSearchResponse {
254    /// Search results with node ID and similarity score.
255    pub results: Vec<GraphSearchResultItem>,
256}
257
258/// A single graph search result.
259#[derive(Debug, Serialize, ToSchema)]
260pub struct GraphSearchResultItem {
261    /// Node ID.
262    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
263    #[cfg_attr(feature = "openapi", schema(value_type = String))]
264    pub id: u64,
265    /// Similarity score.
266    pub score: f32,
267    /// Node payload (if any).
268    pub payload: Option<serde_json::Value>,
269}
270
271// ============================================================================
272// SSE Streaming Types (EPIC-058 US-003)
273// ============================================================================
274
275/// Query parameters for streaming graph traversal.
276#[derive(Debug, Deserialize, IntoParams)]
277pub struct StreamTraverseParams {
278    /// Source node ID to start traversal from.
279    #[serde(deserialize_with = "serde_id::deserialize_id_from_string_or_number")]
280    #[param(example = 123)]
281    pub start_node: u64,
282    /// Traversal algorithm: "bfs" or "dfs".
283    #[serde(default = "default_algorithm")]
284    #[param(example = "bfs")]
285    pub algorithm: String,
286    /// Maximum traversal depth.
287    #[serde(default = "default_stream_max_depth")]
288    #[param(example = 5)]
289    pub max_depth: u32,
290    /// Maximum number of results to stream.
291    #[serde(default = "default_stream_limit")]
292    #[param(example = 1000)]
293    pub limit: usize,
294    /// Filter by relationship types (comma-separated).
295    #[serde(default)]
296    #[param(example = "KNOWS,FOLLOWS")]
297    pub relationship_types: Option<String>,
298}
299
300fn default_algorithm() -> String {
301    "bfs".to_string()
302}
303
304fn default_stream_max_depth() -> u32 {
305    5
306}
307
308fn default_stream_limit() -> usize {
309    1000
310}
311
312/// SSE event: A node reached during traversal.
313#[derive(Debug, Serialize, ToSchema)]
314pub struct StreamNodeEvent {
315    /// Target node ID.
316    #[serde(serialize_with = "serde_id::serialize_id_as_string")]
317    #[cfg_attr(feature = "openapi", schema(value_type = String))]
318    pub id: u64,
319    /// Depth from source.
320    pub depth: u32,
321    /// Path of edge IDs taken to reach this node.
322    #[serde(serialize_with = "serde_id::serialize_ids_as_strings")]
323    #[cfg_attr(feature = "openapi", schema(schema_with = serde_id::ids_array_schema))]
324    pub path: Vec<u64>,
325}
326
327/// SSE event: Periodic statistics update.
328#[derive(Debug, Serialize, ToSchema)]
329pub struct StreamStatsEvent {
330    /// Number of nodes visited so far.
331    pub nodes_visited: usize,
332    /// Elapsed time in milliseconds.
333    pub elapsed_ms: u64,
334}
335
336/// SSE event: Traversal completed.
337#[derive(Debug, Serialize, ToSchema)]
338pub struct StreamDoneEvent {
339    /// Total nodes returned.
340    pub total_nodes: usize,
341    /// Maximum depth reached.
342    pub max_depth_reached: u32,
343    /// Total elapsed time in milliseconds.
344    pub elapsed_ms: u64,
345}
346
347/// SSE event: Error occurred.
348#[derive(Debug, Serialize, ToSchema)]
349pub struct StreamErrorEvent {
350    /// Error message.
351    pub error: String,
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    /// Path edge IDs above 2^53 must serialize as a JSON string array.
359    #[test]
360    fn test_traversal_path_serialized_as_strings() {
361        let above_safe = (1_u64 << 53) + 1; // 9_007_199_254_740_993
362        let item = TraversalResultItem {
363            target_id: 2,
364            depth: 1,
365            path: vec![above_safe],
366        };
367        let json = serde_json::to_value(&item).unwrap();
368        assert_eq!(json["path"], serde_json::json!(["9007199254740993"]));
369    }
370
371    /// Streamed node path edge IDs above 2^53 must serialize as strings.
372    #[test]
373    fn test_stream_node_event_path_serialized_as_strings() {
374        let above_safe = (1_u64 << 53) + 1;
375        let event = StreamNodeEvent {
376            id: 1,
377            depth: 1,
378            path: vec![above_safe],
379        };
380        let json = serde_json::to_value(&event).unwrap();
381        assert_eq!(json["path"], serde_json::json!(["9007199254740993"]));
382    }
383
384    /// Node-list ids above 2^53 must serialize as a JSON string array.
385    #[test]
386    fn test_node_list_ids_serialized_as_strings() {
387        let above_safe = (1_u64 << 53) + 1;
388        let response = NodeListResponse {
389            node_ids: vec![1, above_safe],
390            count: 2,
391        };
392        let json = serde_json::to_value(&response).unwrap();
393        assert_eq!(
394            json["node_ids"],
395            serde_json::json!(["1", "9007199254740993"])
396        );
397    }
398
399    /// Parallel-traverse sources must deserialize from BOTH strings and numbers.
400    #[test]
401    fn test_parallel_sources_accepts_strings_and_numbers() {
402        let from_strings: ParallelTraverseRequest =
403            serde_json::from_value(serde_json::json!({ "sources": ["9007199254740993", "2"] }))
404                .expect("string sources must deserialize");
405        assert_eq!(from_strings.sources, vec![(1_u64 << 53) + 1, 2]);
406
407        let from_numbers: ParallelTraverseRequest =
408            serde_json::from_value(serde_json::json!({ "sources": [3, 4] }))
409                .expect("numeric sources must still deserialize");
410        assert_eq!(from_numbers.sources, vec![3, 4]);
411    }
412}