Skip to main content

fraiseql_server/routes/api/
federation.rs

1//! Federation API endpoints.
2//!
3//! Provides endpoints for:
4//! - Listing subgraphs and their health status
5//! - Exporting federation dependency graphs in multiple formats (JSON, DOT, Mermaid)
6
7use axum::{
8    Json,
9    extract::{Query, State},
10};
11use fraiseql_core::db::traits::DatabaseAdapter;
12use serde::{Deserialize, Serialize};
13
14use crate::routes::{
15    api::types::{ApiError, ApiResponse},
16    graphql::AppState,
17};
18
19/// Response containing federation subgraph information.
20#[derive(Debug, Serialize)]
21pub struct SubgraphsResponse {
22    /// List of federated subgraphs
23    pub subgraphs: Vec<SubgraphInfo>,
24}
25
26/// Information about a single federated subgraph.
27#[derive(Debug, Serialize, Clone)]
28pub struct SubgraphInfo {
29    /// Name of the subgraph
30    pub name:     String,
31    /// GraphQL endpoint URL for the subgraph
32    pub url:      String,
33    /// Entity types managed by this subgraph
34    pub entities: Vec<String>,
35    /// Health status of the subgraph
36    pub healthy:  bool,
37}
38
39/// Federation graph in various formats.
40#[derive(Debug, Serialize)]
41pub struct GraphResponse {
42    /// Format of the graph (json, dot, or mermaid)
43    pub format:  String,
44    /// Graph content in the specified format
45    pub content: String,
46}
47
48/// Graph format query parameter for federation graph endpoint.
49#[derive(Debug, Deserialize)]
50pub struct GraphFormatQuery {
51    /// Output format: json (default), dot, or mermaid
52    #[serde(default = "default_format")]
53    pub format: String,
54}
55
56fn default_format() -> String {
57    "json".to_string()
58}
59
60/// Get list of federation subgraphs.
61///
62/// Returns information about all subgraphs in the federated schema,
63/// including their URLs, managed entities, and health status.
64pub async fn subgraphs_handler<A: DatabaseAdapter>(
65    State(_state): State<AppState<A>>,
66) -> Result<Json<ApiResponse<SubgraphsResponse>>, ApiError> {
67    // In a real implementation, this would:
68    // 1. Extract federation metadata from the schema
69    // 2. Query each subgraph for health status
70    // 3. Return actual subgraph information
71
72    // Placeholder: Return empty list
73    let response = SubgraphsResponse { subgraphs: vec![] };
74
75    Ok(Json(ApiResponse {
76        status: "success".to_string(),
77        data:   response,
78    }))
79}
80
81/// Get federation dependency graph.
82///
83/// Exports the federation structure showing:
84/// - Subgraph relationships
85/// - Entity resolution paths
86/// - Dependencies between subgraphs
87///
88/// Supports multiple output formats:
89/// - **json**: Machine-readable federation structure
90/// - **dot**: Graphviz format for visualization
91/// - **mermaid**: Markdown-compatible graph syntax
92pub async fn graph_handler<A: DatabaseAdapter>(
93    State(_state): State<AppState<A>>,
94    Query(query): Query<GraphFormatQuery>,
95) -> Result<Json<ApiResponse<GraphResponse>>, ApiError> {
96    // Validate format parameter
97    let format = match query.format.as_str() {
98        "json" | "dot" | "mermaid" => query.format,
99        _ => return Err(ApiError::validation_error("format must be 'json', 'dot', or 'mermaid'")),
100    };
101
102    // Generate graph in the requested format
103    let content = generate_federation_graph(&format);
104
105    let response = GraphResponse {
106        format: format.clone(),
107        content,
108    };
109
110    Ok(Json(ApiResponse {
111        status: "success".to_string(),
112        data:   response,
113    }))
114}
115
116/// Generate federation graph in the specified format.
117///
118/// In a real implementation, this would:
119/// 1. Extract subgraph information from schema
120/// 2. Build graph structure from federation metadata
121/// 3. Convert to requested format
122fn generate_federation_graph(format: &str) -> String {
123    match format {
124        "json" => generate_json_graph(),
125        "dot" => generate_dot_graph(),
126        "mermaid" => generate_mermaid_graph(),
127        _ => "{}".to_string(),
128    }
129}
130
131/// Generate JSON representation of federation graph.
132fn generate_json_graph() -> String {
133    r#"{
134  "subgraphs": [],
135  "edges": []
136}"#
137    .to_string()
138}
139
140/// Generate Graphviz (DOT) representation of federation graph.
141fn generate_dot_graph() -> String {
142    r#"digraph federation {
143  rankdir=LR;
144  node [shape=box, style=rounded];
145
146  // Subgraphs would be added here
147  // Example:
148  // users [label="users\n[User, Query]"];
149  // posts [label="posts\n[Post]"];
150  // users -> posts [label="User", style=dashed];
151}"#
152    .to_string()
153}
154
155/// Generate Mermaid diagram representation of federation graph.
156fn generate_mermaid_graph() -> String {
157    r#"graph LR
158    %% Federation subgraphs
159    %% Example:
160    %% users["users<br/>[User, Query]"]
161    %% posts["posts<br/>[Post]"]
162    %% users -->|User| posts"#
163        .to_string()
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_generate_json_graph() {
172        let json = generate_json_graph();
173        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
174
175        assert!(parsed["subgraphs"].is_array());
176        assert!(parsed["edges"].is_array());
177    }
178
179    #[test]
180    fn test_generate_dot_graph() {
181        let dot = generate_dot_graph();
182
183        assert!(dot.contains("digraph"));
184        assert!(dot.contains("rankdir"));
185    }
186
187    #[test]
188    fn test_generate_mermaid_graph() {
189        let mermaid = generate_mermaid_graph();
190
191        assert!(mermaid.contains("graph LR"));
192    }
193
194    #[test]
195    fn test_default_format() {
196        assert_eq!(default_format(), "json");
197    }
198
199    #[test]
200    fn test_subgraph_info_creation() {
201        let info = SubgraphInfo {
202            name:     "test".to_string(),
203            url:      "http://test.local".to_string(),
204            entities: vec!["Entity1".to_string()],
205            healthy:  true,
206        };
207
208        assert_eq!(info.name, "test");
209        assert!(info.healthy);
210    }
211
212    #[test]
213    fn test_subgraphs_response_creation() {
214        let response = SubgraphsResponse { subgraphs: vec![] };
215
216        assert!(response.subgraphs.is_empty());
217    }
218
219    #[test]
220    fn test_graph_response_creation() {
221        let response = GraphResponse {
222            format:  "json".to_string(),
223            content: "{}".to_string(),
224        };
225
226        assert_eq!(response.format, "json");
227    }
228}