use std::collections::BTreeMap;
use axum::{Json, extract::State};
use fraiseql_core::{
db::traits::DatabaseAdapter,
schema::{CompiledSchema, FieldDenyPolicy},
};
use serde::Serialize;
use crate::routes::{api::types::ApiResponse, graphql::AppState};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct FieldSecurityMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_deny: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires_role: Option<String>,
}
impl FieldSecurityMetadata {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.encrypted.is_none()
&& self.requires_scope.is_none()
&& self.on_deny.is_none()
&& self.requires_role.is_none()
}
}
#[non_exhaustive]
#[derive(Debug, Serialize)]
pub struct MetadataResponse {
pub metadata: BTreeMap<String, FieldSecurityMetadata>,
}
pub async fn metadata_handler<A: DatabaseAdapter>(
State(state): State<AppState<A>>,
) -> Json<ApiResponse<MetadataResponse>> {
let metadata = flatten_field_metadata(state.executor().schema());
Json(ApiResponse {
status: "success".to_string(),
data: MetadataResponse { metadata },
})
}
#[must_use]
pub fn flatten_field_metadata(schema: &CompiledSchema) -> BTreeMap<String, FieldSecurityMetadata> {
let mut map = BTreeMap::new();
for type_def in &schema.types {
let type_name = type_def.name.as_str();
if let Some(role) = &type_def.requires_role {
map.insert(
type_name.to_string(),
FieldSecurityMetadata {
encrypted: None,
requires_scope: None,
on_deny: None,
requires_role: Some(role.clone()),
},
);
}
for field in &type_def.fields {
let encrypted = field.encryption.as_ref().map(|_| true);
let requires_scope = field.requires_scope.clone();
let on_deny = match field.on_deny {
FieldDenyPolicy::Mask => Some("mask".to_string()),
_ => None,
};
let meta = FieldSecurityMetadata {
encrypted,
requires_scope,
on_deny,
requires_role: None,
};
if !meta.is_empty() {
map.insert(format!("{type_name}.{}", field.name), meta);
}
}
}
map
}