use axum::{
Json,
extract::{Path, Query, State},
};
use fraiseql_core::db::{DatabaseType, QueryStatEntry};
use serde::{Deserialize, Serialize};
use crate::routes::{
api::types::{ApiError, ApiResponse},
graphql::AppState,
};
#[derive(Debug, Deserialize)]
pub struct QueryStatsParams {
pub limit: Option<u32>,
}
#[derive(Debug, Serialize)]
pub struct QueryStatsResponse {
pub database_type: String,
pub stats_available: bool,
pub entries: Vec<QueryStatEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct QueryStatsDetailResponse {
pub database_type: String,
pub entry: QueryStatEntry,
}
#[derive(Debug, Serialize)]
pub struct QueryStatsResetResponse {
pub message: String,
}
pub async fn query_stats_handler(
State(state): State<AppState>,
Query(params): Query<QueryStatsParams>,
) -> Result<Json<ApiResponse<QueryStatsResponse>>, ApiError> {
let limit = params.limit.unwrap_or(20).clamp(1, 100);
let executor = state.executor();
let db_type = executor.database_type();
let stats_available = matches!(db_type, DatabaseType::PostgreSQL);
let entries = executor
.query_stats(limit)
.await
.map_err(|e| ApiError::internal_error(format!("Failed to fetch query stats: {e}")))?;
let message = if !stats_available {
Some(format!("Query stats are not supported by {db_type}"))
} else if entries.is_empty() {
Some(
"No query stats recorded yet (extension may not be installed or no queries executed)"
.to_string(),
)
} else {
None
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: QueryStatsResponse {
database_type: db_type.to_string(),
stats_available,
entries,
message,
},
}))
}
pub async fn query_stats_detail_handler(
State(state): State<AppState>,
Path(queryid): Path<String>,
) -> Result<Json<ApiResponse<QueryStatsDetailResponse>>, ApiError> {
let executor = state.executor();
let db_type = executor.database_type();
let entry = executor
.query_stats_by_id(&queryid)
.await
.map_err(|e| ApiError::internal_error(format!("Failed to fetch query stats: {e}")))?;
match entry {
Some(entry) => Ok(Json(ApiResponse {
status: "success".to_string(),
data: QueryStatsDetailResponse {
database_type: db_type.to_string(),
entry,
},
})),
None => Err(ApiError::not_found(format!("No query stats found for id '{queryid}'"))),
}
}
pub async fn query_stats_reset_handler(
State(state): State<AppState>,
) -> Result<Json<ApiResponse<QueryStatsResetResponse>>, ApiError> {
let executor = state.executor();
match executor.reset_query_stats().await {
Ok(()) => Ok(Json(ApiResponse {
status: "success".to_string(),
data: QueryStatsResetResponse {
message: "Query statistics have been reset".to_string(),
},
})),
Err(fraiseql_error::FraiseQLError::Unsupported { message }) => {
Err(ApiError::new(message, "UNSUPPORTED_OPERATION".to_string()))
},
Err(e) => Err(ApiError::internal_error(format!("Failed to reset query stats: {e}"))),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests;