fraiseql_server/routes/
introspection.rs1use axum::{Json, extract::State, response::IntoResponse};
4use fraiseql_core::db::traits::DatabaseAdapter;
5use serde::Serialize;
6use tracing::debug;
7
8use crate::{extractors::OptionalSecurityContext, routes::graphql::AppState};
9
10#[derive(Debug, Serialize)]
12pub struct IntrospectionResponse {
13 pub types: Vec<TypeInfo>,
15
16 pub queries: Vec<QueryInfo>,
18
19 pub mutations: Vec<MutationInfo>,
21}
22
23#[derive(Debug, Serialize)]
25pub struct TypeInfo {
26 pub name: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub description: Option<String>,
32
33 pub field_count: usize,
35}
36
37#[derive(Debug, Serialize)]
39pub struct QueryInfo {
40 pub name: String,
42
43 pub return_type: String,
45
46 pub returns_list: bool,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub description: Option<String>,
52}
53
54#[derive(Debug, Serialize)]
56pub struct MutationInfo {
57 pub name: String,
59
60 pub return_type: String,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub description: Option<String>,
66}
67
68pub async fn introspection_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
79 State(state): State<AppState<A>>,
80 OptionalSecurityContext(security_context): OptionalSecurityContext,
81) -> impl IntoResponse {
82 debug!("Introspection requested");
83
84 let executor = state.executor();
85 let schema = executor.schema();
86
87 let user_roles: Vec<&str> = security_context
88 .as_ref()
89 .map(|ctx| ctx.roles.iter().map(String::as_str).collect())
90 .unwrap_or_default();
91
92 let types: Vec<TypeInfo> = schema
93 .types
94 .iter()
95 .filter(|t| t.requires_role.as_ref().is_none_or(|role| user_roles.contains(&role.as_str())))
96 .map(|t| TypeInfo {
97 name: t.name.to_string(),
98 description: t.description.clone(),
99 field_count: t.fields.len(),
100 })
101 .collect();
102
103 let queries: Vec<QueryInfo> = schema
104 .queries
105 .iter()
106 .filter(|q| q.requires_role.as_ref().is_none_or(|role| user_roles.contains(&role.as_str())))
107 .map(|q| QueryInfo {
108 name: schema.display_name(&q.name),
109 return_type: q.return_type.clone(),
110 returns_list: q.returns_list,
111 description: q.description.clone(),
112 })
113 .collect();
114
115 let mutations: Vec<MutationInfo> = schema
116 .mutations
117 .iter()
118 .map(|m| MutationInfo {
119 name: schema.display_name(&m.name),
120 return_type: m.return_type.clone(),
121 description: m.description.clone(),
122 })
123 .collect();
124
125 Json(IntrospectionResponse {
126 types,
127 queries,
128 mutations,
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 #![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::*;
145
146 #[test]
147 fn test_type_info_serialization() {
148 let type_info = TypeInfo {
149 name: "User".to_string(),
150 description: Some("A user in the system".to_string()),
151 field_count: 3,
152 };
153
154 let json = serde_json::to_string(&type_info).unwrap();
155 assert!(json.contains("User"));
156 assert!(json.contains("field_count"));
157 }
158}