linkrs_server/web/handlers/
graph_data.rs1use axum::{
9 extract::{Extension, Path, Query, State},
10 response::Json,
11 routing::get,
12 Router,
13};
14use serde::Deserialize;
15
16use crate::storage::{
17 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
18};
19use crate::web::{
20 error::{WebError, WebResult},
21 handlers::common::{execute_in_space, vid_literal},
22 models::ApiResponse,
23 WebState,
24};
25
26pub fn create_routes<
28 S: StorageClient
29 + StorageSchemaContextOps
30 + StorageSyncContextOps
31 + StorageOperationContextOps
32 + Clone
33 + Send
34 + Sync
35 + 'static,
36>() -> Router<WebState<S>> {
37 Router::new()
38 .route("/vertices/{vid}", get(get_vertex))
39 .route("/edges", get(get_edge))
40 .route("/vertices/{vid}/neighbors", get(get_neighbors))
41}
42
43#[derive(Debug, Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
45#[into_params(parameter_in = Query)]
46pub struct GetVertexParams {
47 pub space: String,
48}
49
50#[utoipa::path(
51 get,
52 path = "/api/v1/graph/vertices/{vid}",
53 tag = "WebGraph",
54 params(
55 ("vid" = String, Path, description = "Vertex id"),
56 GetVertexParams
57 ),
58 responses(
59 (status = 200, description = "Vertex detail", body = ApiResponse<serde_json::Value>),
60 (status = 404, description = "Not found"),
61 (status = 500, description = "Internal error")
62 )
63)]
64async fn get_vertex<
65 S: StorageClient
66 + StorageSchemaContextOps
67 + StorageSyncContextOps
68 + StorageOperationContextOps
69 + Clone
70 + Send
71 + Sync
72 + 'static,
73>(
74 State(web_state): State<WebState<S>>,
75 Extension(caller_session_id): Extension<i64>,
76 Path(vid): Path<String>,
77 Query(params): Query<GetVertexParams>,
78) -> WebResult<Json<ApiResponse<serde_json::Value>>> {
79 let id = vid_literal(&web_state, ¶ms.space, &vid)?;
82 let query = format!("MATCH (v) WHERE id(v) == {} RETURN v LIMIT 1", id);
83
84 let result = execute_in_space(&web_state, caller_session_id, ¶ms.space, &query).await?;
85 if let Some(vertex) = result.first_value() {
86 return Ok(Json(ApiResponse::success(
87 serde_json::json!({"vertex": vertex}),
88 )));
89 }
90
91 Err(WebError::NotFound(format!(
92 "Vertex '{}' not found in space '{}'",
93 vid, params.space
94 )))
95}
96
97#[derive(Debug, Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
99#[into_params(parameter_in = Query)]
100pub struct GetEdgeParams {
101 pub space: String,
102 pub src: String,
103 pub dst: String,
104 pub edge_type: String,
105 #[serde(default)]
106 pub rank: i64,
107}
108
109#[utoipa::path(
110 get,
111 path = "/api/v1/graph/edges",
112 tag = "WebGraph",
113 params(GetEdgeParams),
114 responses(
115 (status = 200, description = "Edge detail", body = ApiResponse<serde_json::Value>),
116 (status = 404, description = "Not found"),
117 (status = 500, description = "Internal error")
118 )
119)]
120async fn get_edge<
121 S: StorageClient
122 + StorageSchemaContextOps
123 + StorageSyncContextOps
124 + StorageOperationContextOps
125 + Clone
126 + Send
127 + Sync
128 + 'static,
129>(
130 State(web_state): State<WebState<S>>,
131 Extension(caller_session_id): Extension<i64>,
132 Query(params): Query<GetEdgeParams>,
133) -> WebResult<Json<ApiResponse<serde_json::Value>>> {
134 let query = format!(
136 "FETCH PROP ON {} \"{}\" -> \"{}\"@{} YIELD edge AS e",
137 params.edge_type, params.src, params.dst, params.rank
138 );
139
140 let result = execute_in_space(&web_state, caller_session_id, ¶ms.space, &query).await?;
141 if let Some(edge) = result.first_value() {
142 return Ok(Json(ApiResponse::success(
143 serde_json::json!({"edge": edge}),
144 )));
145 }
146
147 Err(WebError::NotFound(format!(
148 "Edge from '{}' to '{}' with type '{}' not found in space '{}'",
149 params.src, params.dst, params.edge_type, params.space
150 )))
151}
152
153#[derive(Debug, Deserialize, utoipa::IntoParams, utoipa::ToSchema)]
155#[into_params(parameter_in = Query)]
156pub struct GetNeighborsParams {
157 pub space: String,
158 #[serde(default = "default_direction")]
160 pub direction: String,
161 pub edge_type: Option<String>,
163}
164
165fn default_direction() -> String {
166 "BOTH".to_string()
167}
168
169#[utoipa::path(
170 get,
171 path = "/api/v1/graph/vertices/{vid}/neighbors",
172 tag = "WebGraph",
173 params(
174 ("vid" = String, Path, description = "Vertex id"),
175 GetNeighborsParams
176 ),
177 responses(
178 (status = 200, description = "Neighbor list", body = ApiResponse<serde_json::Value>),
179 (status = 500, description = "Internal error")
180 )
181)]
182async fn get_neighbors<
183 S: StorageClient
184 + StorageSchemaContextOps
185 + StorageSyncContextOps
186 + StorageOperationContextOps
187 + Clone
188 + Send
189 + Sync
190 + 'static,
191>(
192 State(web_state): State<WebState<S>>,
193 Extension(caller_session_id): Extension<i64>,
194 Path(vid): Path<String>,
195 Query(params): Query<GetNeighborsParams>,
196) -> WebResult<Json<ApiResponse<serde_json::Value>>> {
197 let edge = match ¶ms.edge_type {
200 Some(edge_type) => format!("[:{}]", edge_type),
201 None => "[]".to_string(),
202 };
203 let pattern = match params.direction.as_str() {
204 "OUT" => format!("(v)-{}->(n)", edge),
205 "IN" => format!("(v)<-{}-(n)", edge),
206 _ => format!("(v)-{}-(n)", edge),
207 };
208
209 let id = vid_literal(&web_state, ¶ms.space, &vid)?;
210 let query = format!("MATCH {} WHERE id(v) == {} RETURN n LIMIT 100", pattern, id);
211
212 let result = execute_in_space(&web_state, caller_session_id, ¶ms.space, &query).await?;
213 let neighbors: Vec<serde_json::Value> = result
214 .rows()
215 .iter()
216 .flat_map(|row| row.iter())
217 .filter_map(|v| match v {
218 linkrs_core::Value::Vertex(vertex) => Some(serde_json::json!({"vertex": vertex})),
219 _ => None,
220 })
221 .collect();
222
223 Ok(Json(ApiResponse::success(serde_json::json!({
224 "vid": vid,
225 "space": params.space,
226 "direction": params.direction,
227 "edge_type": params.edge_type,
228 "neighbors": neighbors,
229 }))))
230}