Skip to main content

fraiseql_server/routes/api/
usage.rs

1//! Usage statistics API endpoint.
2//!
3//! Exposes in-memory mutation counters accumulated by the `MutationAuditLayer`
4//! tracing subscriber.  Counters are keyed by `(tenant_id, period, entity_type)`
5//! and reset to zero on process restart.
6//!
7//! ## Endpoint
8//!
9//! ```text
10//! GET /api/v1/admin/usage?tenant_id=<str>&period=<YYYY-MM>
11//! ```
12//!
13//! Protected by the admin bearer token (same as all other admin read routes).
14//!
15//! ## Response
16//!
17//! ```json
18//! {
19//!   "tenant_id": "acme",
20//!   "period": "2026-05",
21//!   "usage": {
22//!     "mutations": { "User": 42, "Order": 7 }
23//!   }
24//! }
25//! ```
26//!
27//! Unknown `tenant_id` / `period` combinations return 200 with
28//! `"usage": { "mutations": {} }` — never 404.
29//!
30//! Invalid `period` (not `YYYY-MM`) returns 400 with
31//! `{"error": "invalid period format"}`.
32
33use axum::{
34    Json,
35    extract::{Query, State},
36    http::StatusCode,
37};
38use fraiseql_core::db::traits::DatabaseAdapter;
39use serde::{Deserialize, Serialize};
40
41use crate::{
42    routes::graphql::AppState,
43    usage::aggregator::{UsageSummary, validate_period},
44};
45
46// ── Query parameters ───────────────────────────────────────────────────────
47
48/// Query parameters for the usage endpoint.
49#[non_exhaustive]
50#[derive(Debug, Deserialize)]
51pub struct UsageQueryParams {
52    /// Tenant identifier to query.
53    pub tenant_id: String,
54    /// Billing period in `YYYY-MM` format (e.g. `"2026-05"`).
55    pub period:    String,
56}
57
58// ── Response types ─────────────────────────────────────────────────────────
59
60/// Successful usage query response.
61#[non_exhaustive]
62#[derive(Debug, Serialize)]
63pub struct UsageResponse {
64    /// The queried tenant identifier.
65    pub tenant_id: String,
66    /// The queried period (`YYYY-MM`).
67    pub period:    String,
68    /// Mutation counts for the queried period.
69    pub usage:     UsageSummary,
70}
71
72// ── Handler ────────────────────────────────────────────────────────────────
73
74/// Query mutation usage statistics for a tenant and period.
75///
76/// Returns 400 when `period` is not a valid `YYYY-MM` string.  Returns 200
77/// with empty `mutations` for unknown tenant/period combinations.
78///
79/// # Errors
80///
81/// Returns `(400, {"error": "invalid period format"})` when the `period`
82/// query parameter is not in `YYYY-MM` format.
83pub async fn usage_handler<A: DatabaseAdapter>(
84    State(state): State<AppState<A>>,
85    Query(params): Query<UsageQueryParams>,
86) -> Result<Json<UsageResponse>, (StatusCode, Json<serde_json::Value>)> {
87    if !validate_period(&params.period) {
88        return Err((
89            StatusCode::BAD_REQUEST,
90            Json(serde_json::json!({"error": "invalid period format"})),
91        ));
92    }
93
94    let usage = state.usage.query(&params.tenant_id, &params.period);
95
96    Ok(Json(UsageResponse {
97        tenant_id: params.tenant_id,
98        period: params.period,
99        usage,
100    }))
101}
102
103// ── Tests ──────────────────────────────────────────────────────────────────