1use std::sync::Arc;
7
8use axum::{
9 extract::{Path, Query, State},
10 http::StatusCode,
11 Json,
12};
13use velesdb_core::collection::graph::{GraphEdge, TraversalConfig};
14
15use crate::types::ErrorResponse;
16use crate::AppState;
17
18use super::types::{
19 AddEdgeRequest, DegreeResponse, EdgeQueryParams, EdgeResponse, EdgesResponse, TraversalStats,
20 TraverseRequest, TraverseResponse,
21};
22
23#[allow(deprecated)]
31fn get_graph_collection_or_404(
32 state: &AppState,
33 name: &str,
34) -> Result<velesdb_core::GraphCollection, (StatusCode, Json<ErrorResponse>)> {
35 if let Some(c) = state.db.get_graph_collection(name) {
37 return Ok(c);
38 }
39
40 if state.db.get_collection(name).is_some() {
43 return Err((
44 StatusCode::CONFLICT,
45 Json(ErrorResponse {
46 error: format!(
47 "Collection '{}' exists but is not a graph collection. \
48 Use /collections/{}/graph only on graph-typed collections.",
49 name, name
50 ),
51 code: None,
52 }),
53 ));
54 }
55
56 use velesdb_core::GraphSchema;
58 state
59 .db
60 .create_graph_collection(name, GraphSchema::schemaless())
61 .map_err(|e| {
62 (
63 StatusCode::INTERNAL_SERVER_ERROR,
64 Json(ErrorResponse {
65 error: format!("Failed to auto-create graph collection '{}': {e}", name),
66 code: None,
67 }),
68 )
69 })?;
70
71 state.db.get_graph_collection(name).ok_or_else(|| {
72 (
73 StatusCode::INTERNAL_SERVER_ERROR,
74 Json(ErrorResponse {
75 error: format!("Graph collection '{}' not found after creation.", name),
76 code: None,
77 }),
78 )
79 })
80}
81
82#[utoipa::path(
84 get,
85 path = "/collections/{name}/graph/edges",
86 params(
87 ("name" = String, Path, description = "Collection name"),
88 EdgeQueryParams
89 ),
90 responses(
91 (status = 200, description = "Edges retrieved successfully", body = EdgesResponse),
92 (status = 400, description = "Missing required 'label' query parameter", body = ErrorResponse),
93 (status = 404, description = "Collection not found", body = ErrorResponse),
94 (status = 500, description = "Internal server error", body = ErrorResponse)
95 ),
96 tag = "graph"
97)]
98pub async fn get_edges(
99 Path(name): Path<String>,
100 Query(params): Query<EdgeQueryParams>,
101 State(state): State<Arc<AppState>>,
102) -> Result<Json<EdgesResponse>, (StatusCode, Json<ErrorResponse>)> {
103 let label = params.label.ok_or_else(|| {
104 (
105 StatusCode::BAD_REQUEST,
106 Json(ErrorResponse {
107 error: "Query parameter 'label' is required. Listing all edges requires pagination (not yet implemented).".to_string(),
108 code: None,
109 }),
110 )
111 })?;
112
113 let coll = get_graph_collection_or_404(&state, &name)?;
114
115 let edges: Vec<EdgeResponse> = coll
116 .get_edges(Some(&label))
117 .into_iter()
118 .map(|e| EdgeResponse {
119 id: e.id(),
120 source: e.source(),
121 target: e.target(),
122 label: e.label().to_string(),
123 properties: serde_json::to_value(e.properties()).unwrap_or_default(),
124 })
125 .collect();
126
127 let count = edges.len();
128 Ok(Json(EdgesResponse { edges, count }))
129}
130
131#[utoipa::path(
133 post,
134 path = "/collections/{name}/graph/edges",
135 request_body = AddEdgeRequest,
136 responses(
137 (status = 201, description = "Edge added successfully"),
138 (status = 400, description = "Invalid request", body = ErrorResponse),
139 (status = 404, description = "Collection not found", body = ErrorResponse),
140 (status = 500, description = "Internal server error", body = ErrorResponse)
141 ),
142 tag = "graph"
143)]
144pub async fn add_edge(
145 Path(name): Path<String>,
146 State(state): State<Arc<AppState>>,
147 Json(request): Json<AddEdgeRequest>,
148) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
149 let properties: std::collections::HashMap<String, serde_json::Value> = match request.properties
150 {
151 serde_json::Value::Object(map) => map.into_iter().collect(),
152 serde_json::Value::Null => std::collections::HashMap::new(),
153 _ => {
154 return Err((
155 StatusCode::BAD_REQUEST,
156 Json(ErrorResponse {
157 error: "Properties must be an object or null".to_string(),
158 code: None,
159 }),
160 ));
161 }
162 };
163
164 let edge = GraphEdge::new(request.id, request.source, request.target, &request.label)
165 .map_err(|e| {
166 (
167 StatusCode::BAD_REQUEST,
168 Json(ErrorResponse {
169 error: format!("Invalid edge: {e}"),
170 code: None,
171 }),
172 )
173 })?
174 .with_properties(properties);
175
176 let coll = get_graph_collection_or_404(&state, &name)?;
177
178 coll.add_edge(edge).map_err(|e| {
179 (
180 StatusCode::INTERNAL_SERVER_ERROR,
181 Json(ErrorResponse {
182 error: format!("Failed to add edge: {e}"),
183 code: None,
184 }),
185 )
186 })?;
187
188 Ok(StatusCode::CREATED)
189}
190
191#[utoipa::path(
193 post,
194 path = "/collections/{name}/graph/traverse",
195 request_body = TraverseRequest,
196 responses(
197 (status = 200, description = "Traversal completed successfully", body = TraverseResponse),
198 (status = 400, description = "Invalid request", body = ErrorResponse),
199 (status = 404, description = "Collection not found", body = ErrorResponse),
200 (status = 500, description = "Internal server error", body = ErrorResponse)
201 ),
202 tag = "graph"
203)]
204pub async fn traverse_graph(
205 Path(name): Path<String>,
206 State(state): State<Arc<AppState>>,
207 Json(request): Json<TraverseRequest>,
208) -> Result<Json<TraverseResponse>, (StatusCode, Json<ErrorResponse>)> {
209 let coll = get_graph_collection_or_404(&state, &name)?;
210
211 let config = TraversalConfig::with_range(1, request.max_depth)
212 .with_limit(request.limit)
213 .with_rel_types(request.rel_types);
214
215 let raw_results = match request.strategy.to_lowercase().as_str() {
216 "bfs" => coll.traverse_bfs(request.source, &config),
217 "dfs" => coll.traverse_dfs(request.source, &config),
218 _ => {
219 return Err((
220 StatusCode::BAD_REQUEST,
221 Json(ErrorResponse {
222 error: format!(
223 "Invalid strategy '{}'. Use 'bfs' or 'dfs'.",
224 request.strategy
225 ),
226 code: None,
227 }),
228 ));
229 }
230 };
231
232 let results: Vec<super::types::TraversalResultItem> = raw_results
234 .into_iter()
235 .map(|r| super::types::TraversalResultItem {
236 target_id: r.target_id,
237 depth: r.depth,
238 path: r.path,
239 })
240 .collect();
241
242 let depth_reached = results.iter().map(|r| r.depth).max().unwrap_or(0);
243 let visited = results.len();
244 let has_more = visited >= request.limit;
245
246 Ok(Json(TraverseResponse {
247 results,
248 next_cursor: None,
249 has_more,
250 stats: TraversalStats {
251 visited,
252 depth_reached,
253 },
254 }))
255}
256
257#[utoipa::path(
259 get,
260 path = "/collections/{name}/graph/nodes/{node_id}/degree",
261 params(
262 ("name" = String, Path, description = "Collection name"),
263 ("node_id" = u64, Path, description = "Node ID")
264 ),
265 responses(
266 (status = 200, description = "Degree retrieved successfully", body = DegreeResponse),
267 (status = 404, description = "Collection not found", body = ErrorResponse),
268 (status = 500, description = "Internal server error", body = ErrorResponse)
269 ),
270 tag = "graph"
271)]
272pub async fn get_node_degree(
273 Path((name, node_id)): Path<(String, u64)>,
274 State(state): State<Arc<AppState>>,
275) -> Result<Json<DegreeResponse>, (StatusCode, Json<ErrorResponse>)> {
276 let coll = get_graph_collection_or_404(&state, &name)?;
277 let (in_degree, out_degree) = coll.node_degree(node_id);
278 Ok(Json(DegreeResponse {
279 in_degree,
280 out_degree,
281 }))
282}