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,
}
pub(crate) 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(),
}
}
pub(crate) 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())
}
pub(crate) 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
}
#[derive(Debug, Deserialize)]
pub struct PlanQuery {
pub query: String,
}
#[derive(Debug, Serialize)]
pub struct PlanResponse {
pub cached: bool,
pub schema_fingerprint: String,
#[cfg(feature = "federation")]
pub fetches: Option<Vec<fraiseql_core::federation::SubgraphFetch>>,
#[cfg(not(feature = "federation"))]
pub fetches: Option<serde_json::Value>,
}
#[cfg(feature = "federation")]
pub async fn plan_handler<A: DatabaseAdapter>(
State(state): State<AppState<A>>,
Query(params): Query<PlanQuery>,
) -> Result<Json<ApiResponse<PlanResponse>>, ApiError> {
if params.query.is_empty() {
return Err(ApiError::validation_error("query parameter is required"));
}
if params.query.len() > state.max_get_query_bytes {
return Err(ApiError::validation_error("query parameter too long"));
}
let Some(ref plan_cache) = state.federation_plan_cache else {
let response = PlanResponse {
cached: false,
schema_fingerprint: String::new(),
fetches: None,
};
return Ok(Json(ApiResponse {
status: "success".to_string(),
data: response,
}));
};
let normalized = fraiseql_core::federation::query_plan_cache::normalize_query(¶ms.query);
let executor = state.executor();
let schema = executor.schema();
let fingerprint =
schema.federation.as_ref().and_then(|f| f.version.clone()).unwrap_or_default();
let plan = plan_cache.get(&normalized, &fingerprint);
let response = match plan {
Some(plan) => PlanResponse {
cached: true,
schema_fingerprint: plan.schema_fingerprint.clone(),
fetches: Some(plan.fetches),
},
None => PlanResponse {
cached: false,
schema_fingerprint: fingerprint,
fetches: None,
},
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: response,
}))
}
pub(crate) 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
}