use axum::{
Json,
extract::{Path, Query, State},
};
use fraiseql_core::db::{DatabaseType, QueryStatEntry, traits::DatabaseAdapter};
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<A: DatabaseAdapter + 'static>(
State(state): State<AppState<A>>,
Query(params): Query<QueryStatsParams>,
) -> Result<Json<ApiResponse<QueryStatsResponse>>, ApiError> {
let limit = params.limit.unwrap_or(20).clamp(1, 100);
let executor = state.executor();
let adapter = executor.adapter();
let db_type = adapter.database_type();
let stats_available = !matches!(db_type, DatabaseType::SQLite);
let entries = adapter
.query_stats(limit)
.await
.map_err(|e| ApiError::internal_error(format!("Failed to fetch query stats: {e}")))?;
let message = if !stats_available {
Some("Query stats are not supported by SQLite".to_string())
} 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<A: DatabaseAdapter + 'static>(
State(state): State<AppState<A>>,
Path(queryid): Path<String>,
) -> Result<Json<ApiResponse<QueryStatsDetailResponse>>, ApiError> {
let executor = state.executor();
let adapter = executor.adapter();
let db_type = adapter.database_type();
let entry = adapter
.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<A: DatabaseAdapter + 'static>(
State(state): State<AppState<A>>,
) -> Result<Json<ApiResponse<QueryStatsResetResponse>>, ApiError> {
let executor = state.executor();
let adapter = executor.adapter();
match adapter.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;