use axum::{Json, extract::State, response::IntoResponse};
use fraiseql_core::db::traits::DatabaseAdapter;
use serde::Serialize;
use tracing::debug;
use crate::{extractors::OptionalSecurityContext, routes::graphql::AppState};
#[derive(Debug, Serialize)]
pub struct IntrospectionResponse {
pub types: Vec<TypeInfo>,
pub queries: Vec<QueryInfo>,
pub mutations: Vec<MutationInfo>,
}
#[derive(Debug, Serialize)]
pub struct TypeInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub field_count: usize,
}
#[derive(Debug, Serialize)]
pub struct QueryInfo {
pub name: String,
pub return_type: String,
pub returns_list: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct MutationInfo {
pub name: String,
pub return_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
pub async fn introspection_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
State(state): State<AppState<A>>,
OptionalSecurityContext(security_context): OptionalSecurityContext,
) -> impl IntoResponse {
debug!("Introspection requested");
let executor = state.executor();
let schema = executor.schema();
let user_roles: Vec<&str> = security_context
.as_ref()
.map(|ctx| ctx.roles.iter().map(String::as_str).collect())
.unwrap_or_default();
let types: Vec<TypeInfo> = schema
.types
.iter()
.filter(|t| t.requires_role.as_ref().is_none_or(|role| user_roles.contains(&role.as_str())))
.map(|t| TypeInfo {
name: t.name.to_string(),
description: t.description.clone(),
field_count: t.fields.len(),
})
.collect();
let queries: Vec<QueryInfo> = schema
.queries
.iter()
.filter(|q| q.requires_role.as_ref().is_none_or(|role| user_roles.contains(&role.as_str())))
.map(|q| QueryInfo {
name: schema.display_name(&q.name),
return_type: q.return_type.clone(),
returns_list: q.returns_list,
description: q.description.clone(),
})
.collect();
let mutations: Vec<MutationInfo> = schema
.mutations
.iter()
.map(|m| MutationInfo {
name: schema.display_name(&m.name),
return_type: m.return_type.clone(),
description: m.description.clone(),
})
.collect();
Json(IntrospectionResponse {
types,
queries,
mutations,
})
}
#[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_type_info_serialization() {
let type_info = TypeInfo {
name: "User".to_string(),
description: Some("A user in the system".to_string()),
field_count: 3,
};
let json = serde_json::to_string(&type_info).unwrap();
assert!(json.contains("User"));
assert!(json.contains("field_count"));
}
}