mockforge_registry_server/handlers/
graph.rs1use axum::{
12 extract::{Path, State},
13 http::HeaderMap,
14 Json,
15};
16use serde::Serialize;
17use uuid::Uuid;
18
19use crate::{
20 error::{ApiError, ApiResult},
21 middleware::{resolve_org_context, AuthUser},
22 models::{cloud_service::CloudService, CloudWorkspace, Flow},
23 AppState,
24};
25
26#[derive(Debug, Serialize)]
27pub struct GraphData {
28 pub nodes: Vec<GraphNode>,
29 pub edges: Vec<GraphEdge>,
30 pub clusters: Vec<GraphCluster>,
31}
32
33#[derive(Debug, Serialize)]
34pub struct GraphNode {
35 pub id: String,
36 pub label: String,
37 #[serde(rename = "nodeType")]
38 pub node_type: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub protocol: Option<String>,
41 pub metadata: serde_json::Value,
42}
43
44#[derive(Debug, Serialize)]
45pub struct GraphEdge {
46 pub from: String,
47 pub to: String,
48 #[serde(rename = "edgeType")]
49 pub edge_type: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub label: Option<String>,
52 pub metadata: serde_json::Value,
53}
54
55#[derive(Debug, Serialize)]
56pub struct GraphCluster {
57 pub id: String,
58 pub label: String,
59 #[serde(rename = "clusterType")]
60 pub cluster_type: String,
61 #[serde(rename = "nodeIds")]
62 pub node_ids: Vec<String>,
63 pub metadata: serde_json::Value,
64}
65
66pub async fn get_workspace_graph(
67 State(state): State<AppState>,
68 AuthUser(user_id): AuthUser,
69 Path(workspace_id): Path<Uuid>,
70 headers: HeaderMap,
71) -> ApiResult<Json<GraphData>> {
72 let workspace = authorize_workspace(&state, user_id, &headers, workspace_id).await?;
73
74 let services = CloudService::find_by_workspace(state.db.pool(), workspace.org_id, workspace_id)
75 .await
76 .map_err(ApiError::Database)?;
77 let flows = Flow::list_by_workspace(state.db.pool(), workspace_id, None)
78 .await
79 .map_err(ApiError::Database)?;
80
81 let mut nodes: Vec<GraphNode> = Vec::with_capacity(services.len() + flows.len());
82 let mut node_ids: Vec<String> = Vec::with_capacity(services.len() + flows.len());
83
84 for s in &services {
85 let id = format!("service:{}", s.id);
86 node_ids.push(id.clone());
87 nodes.push(GraphNode {
88 id,
89 label: s.name.clone(),
90 node_type: "service".into(),
91 protocol: derive_service_protocol(s),
92 metadata: serde_json::json!({
93 "kind": "service",
94 "description": s.description,
95 "base_url": s.base_url,
96 "enabled": s.enabled,
97 }),
98 });
99 }
100
101 for f in &flows {
102 let id = format!("flow:{}", f.id);
103 node_ids.push(id.clone());
104 nodes.push(GraphNode {
105 id,
106 label: f.name.clone(),
107 node_type: "service".into(),
108 protocol: None,
109 metadata: serde_json::json!({
110 "kind": "flow",
111 "flow_kind": f.kind,
112 "description": f.description,
113 }),
114 });
115 }
116
117 let clusters = vec![GraphCluster {
118 id: format!("workspace:{}", workspace_id),
119 label: workspace.name.clone(),
120 cluster_type: "workspace".into(),
121 node_ids,
122 metadata: serde_json::json!({ "workspace_id": workspace_id }),
123 }];
124
125 Ok(Json(GraphData {
126 nodes,
127 edges: Vec::new(),
128 clusters,
129 }))
130}
131
132fn derive_service_protocol(s: &CloudService) -> Option<String> {
133 s.routes
134 .as_array()
135 .and_then(|arr| arr.first())
136 .and_then(|r| r.get("protocol"))
137 .and_then(|p| p.as_str())
138 .map(str::to_lowercase)
139}
140
141async fn authorize_workspace(
142 state: &AppState,
143 user_id: Uuid,
144 headers: &HeaderMap,
145 workspace_id: Uuid,
146) -> ApiResult<CloudWorkspace> {
147 let workspace = CloudWorkspace::find_by_id(state.db.pool(), workspace_id)
148 .await?
149 .ok_or_else(|| ApiError::InvalidRequest("Workspace not found".into()))?;
150 let ctx = resolve_org_context(state, user_id, headers, None)
151 .await
152 .map_err(|_| ApiError::InvalidRequest("Organization not found".into()))?;
153 if ctx.org_id != workspace.org_id {
154 return Err(ApiError::InvalidRequest("Workspace not found".into()));
155 }
156 Ok(workspace)
157}