use axum::{
Json,
extract::{Query, State},
};
use fraiseql_core::db::traits::DatabaseAdapter;
use serde::{Deserialize, Serialize};
use crate::routes::{
api::types::{ApiError, ApiResponse},
graphql::AppState,
};
#[derive(Debug, Serialize)]
pub struct SubgraphsResponse {
pub subgraphs: Vec<SubgraphInfo>,
}
#[derive(Debug, Serialize, Clone)]
pub struct SubgraphInfo {
pub name: String,
pub url: String,
pub entities: Vec<String>,
pub healthy: bool,
}
#[derive(Debug, Serialize)]
pub struct GraphResponse {
pub format: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct GraphFormatQuery {
#[serde(default = "default_format")]
pub format: String,
}
fn default_format() -> String {
"json".to_string()
}
pub async fn subgraphs_handler<A: DatabaseAdapter>(
State(state): State<AppState<A>>,
) -> Result<Json<ApiResponse<SubgraphsResponse>>, ApiError> {
let executor = state.executor();
let schema = executor.schema();
let federation = schema.federation.as_ref();
let subgraphs = match federation {
Some(fed) if fed.enabled => {
let service_name =
fed.service_name.clone().unwrap_or_else(|| "this-service".to_string());
let url = fed.schema_url.clone().unwrap_or_else(|| "/__subgraph_schema".to_string());
let entities = fed.entities.iter().map(|e| e.name.clone()).collect();
vec![SubgraphInfo {
name: service_name,
url,
entities,
healthy: true,
}]
},
_ => vec![],
};
let response = SubgraphsResponse { subgraphs };
Ok(Json(ApiResponse {
status: "success".to_string(),
data: response,
}))
}
pub async fn graph_handler<A: DatabaseAdapter>(
State(state): State<AppState<A>>,
Query(query): Query<GraphFormatQuery>,
) -> Result<Json<ApiResponse<GraphResponse>>, ApiError> {
let format = match query.format.as_str() {
"json" | "dot" | "mermaid" => query.format,
_ => return Err(ApiError::validation_error("format must be 'json', 'dot', or 'mermaid'")),
};
let executor = state.executor();
let schema = executor.schema();
let federation = schema.federation.as_ref();
let content = generate_federation_graph(&format, federation);
let response = GraphResponse { format, content };
Ok(Json(ApiResponse {
status: "success".to_string(),
data: response,
}))
}
fn generate_federation_graph(
format: &str,
federation: Option<&fraiseql_core::schema::FederationConfig>,
) -> String {
match format {
"json" => generate_json_graph(federation),
"dot" => generate_dot_graph(federation),
"mermaid" => generate_mermaid_graph(federation),
_ => "{}".to_string(),
}
}
fn generate_json_graph(federation: Option<&fraiseql_core::schema::FederationConfig>) -> String {
let subgraphs: Vec<serde_json::Value> = match federation {
Some(fed) if fed.enabled => {
let name = fed.service_name.clone().unwrap_or_else(|| "this-service".to_string());
let url = fed.schema_url.clone().unwrap_or_else(|| "/__subgraph_schema".to_string());
let entities: Vec<_> = fed.entities.iter().map(|e| e.name.as_str()).collect();
vec![serde_json::json!({ "name": name, "url": url, "entities": entities })]
},
_ => vec![],
};
serde_json::to_string_pretty(&serde_json::json!({
"subgraphs": subgraphs,
"edges": []
}))
.unwrap_or_else(|_| r#"{"subgraphs":[],"edges":[]}"#.to_string())
}
fn generate_dot_graph(federation: Option<&fraiseql_core::schema::FederationConfig>) -> String {
use std::fmt::Write as _;
let mut dot =
"digraph federation {\n rankdir=LR;\n node [shape=box, style=rounded];\n\n".to_string();
if let Some(fed) = federation {
if fed.enabled {
let name = fed.service_name.clone().unwrap_or_else(|| "this_service".to_string());
let entities: Vec<_> = fed.entities.iter().map(|e| e.name.as_str()).collect();
let label = format!("{}\\n[{}]", name, entities.join(", "));
let _ = writeln!(dot, " {name} [label=\"{label}\"];");
}
}
dot.push('}');
dot
}
fn generate_mermaid_graph(federation: Option<&fraiseql_core::schema::FederationConfig>) -> String {
use std::fmt::Write as _;
let mut mermaid = "graph LR\n".to_string();
if let Some(fed) = federation {
if fed.enabled {
let name = fed.service_name.clone().unwrap_or_else(|| "this-service".to_string());
let entities: Vec<_> = fed.entities.iter().map(|e| e.name.as_str()).collect();
let _ = writeln!(mermaid, " {name}[\"{name}<br/>[{}]\"]", entities.join(", "));
}
}
mermaid
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_errors_doc)] #![allow(missing_docs)] #![allow(clippy::items_after_statements)]
use super::*;
#[test]
fn test_default_format() {
assert_eq!(default_format(), "json");
}
#[test]
fn test_subgraph_info_creation() {
let info = SubgraphInfo {
name: "test".to_string(),
url: "http://test.local".to_string(),
entities: vec!["Entity1".to_string()],
healthy: true,
};
assert_eq!(info.name, "test");
assert!(info.healthy);
}
#[test]
fn test_subgraphs_response_creation() {
let response = SubgraphsResponse { subgraphs: vec![] };
assert!(response.subgraphs.is_empty());
}
#[test]
fn test_graph_response_creation() {
let response = GraphResponse {
format: "json".to_string(),
content: "{}".to_string(),
};
assert_eq!(response.format, "json");
}
#[test]
fn test_generate_json_graph_no_federation() {
let json = generate_json_graph(None);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["subgraphs"].as_array().unwrap().is_empty());
assert!(parsed["edges"].as_array().unwrap().is_empty());
}
#[test]
fn test_generate_dot_graph_no_federation() {
let dot = generate_dot_graph(None);
assert!(dot.contains("digraph"));
assert!(dot.contains("rankdir"));
}
#[test]
fn test_generate_mermaid_graph_no_federation() {
let mermaid = generate_mermaid_graph(None);
assert!(mermaid.contains("graph LR"));
}
}