use axum::{ extract::{Path, Query, State}, http::StatusCode, response::Json, }; use chrono::NaiveTime; use sqlx::PgPool; use uuid::Uuid; use crate::models::{AllQuery, DateQuery, MeasurementData, MeasurementView, Unit}; use crate::{error::AppError, models::MetricType}; pub async fn index() -> &'static str { "Hello World!" } pub async fn all( State(pool): State, Query(options): Query, ) -> Result>, AppError> { let limit = match options.limit { Some(x) if (x > 0) => Some(x), None => None, _ => return Err(AppError::BadRequest), }; // SQL can't bind ASC/DESC as a parameter. The CASE yields measured_at only when // `reverse` is true (sorted DESC) and NULL otherwise, so that key ties for every // row and the ASC key decides the order. If this gets slow, use an if/else with a // separate ASC and DESC query and add an index on measured_at. let rows = sqlx::query_as!( MeasurementView, " SELECT id, measured_at, metric_type, value, unit FROM measurement ORDER BY CASE WHEN $2 THEN measured_at END DESC, measured_at ASC LIMIT $1 ", limit, options.reverse.unwrap_or(false) ) .fetch_all(&pool) .await?; Ok(Json(rows)) } pub async fn add( State(pool): State, Json(data): Json, ) -> Result<(StatusCode, Json), AppError> { let rec = sqlx::query!( " INSERT INTO measurement (id, measured_at, metric_type, value, unit) VALUES ($1, $2, $3, $4, $5) RETURNING id ", Uuid::now_v7(), data.measured_at, data.metric_type, data.value, data.unit ) .fetch_one(&pool) .await?; Ok((StatusCode::CREATED, Json(rec.id))) } pub async fn search( State(pool): State, Query(date_query): Query, ) -> Result>, AppError> { let (from, to) = match (date_query.date, date_query.from, date_query.to) { (Some(date), None, None) => (Some(date), Some(date)), (None, from, to) => (from, to), _ => return Err(AppError::BadRequest), }; if from.is_none() && to.is_none() { return Err(AppError::BadRequest); } if let (Some(from), Some(to)) = (from, to) && from > to { return Err(AppError::BadRequest); } let start = from.map(|from| from.and_time(NaiveTime::MIN).and_utc()); let end = match to { Some(to) => { let Some(next_day) = to.succ_opt() else { return Err(AppError::BadRequest); }; Some(next_day.and_time(NaiveTime::MIN).and_utc()) } None => None, }; let rows = sqlx::query_as!( MeasurementView, "SELECT id, measured_at, metric_type, value, unit FROM measurement WHERE ($1::timestamptz IS NULL OR measured_at >= $1) AND ($2::timestamptz IS NULL OR measured_at < $2)", start, end ) .fetch_all(&pool) .await?; Ok(Json(rows)) } pub async fn units(State(pool): State) -> Result>, AppError> { Ok(Json( sqlx::query_scalar("SELECT enum_range(NULL::unit)") .fetch_one(&pool) .await?, )) } pub async fn metric_types(State(pool): State) -> Result>, AppError> { Ok(Json( sqlx::query_scalar("SELECT enum_range(NULL::metric_type)") .fetch_one(&pool) .await?, )) } pub async fn measurement_by_id( State(pool): State, Path(id): Path, ) -> Result, AppError> { let measurement = sqlx::query_as!( MeasurementView, " SELECT id, measured_at, metric_type, value, unit FROM measurement WHERE id = $1 ", id ) .fetch_one(&pool) .await?; Ok(Json(measurement)) } pub async fn delete( State(pool): State, Path(id): Path, ) -> Result { sqlx::query!("DELETE FROM measurement WHERE id = $1", id) .execute(&pool) .await?; Ok(StatusCode::NO_CONTENT) } pub async fn update( State(pool): State, Path(id): Path, Json(data): Json, ) -> Result { let result = sqlx::query!( " UPDATE measurement SET measured_at = $1, metric_type = $2, value = $3, unit = $4 WHERE id = $5 ", data.measured_at, data.metric_type, data.value, data.unit, id ) .execute(&pool) .await?; if result.rows_affected() == 0 { return Err(AppError::NotFound); } Ok(StatusCode::NO_CONTENT) }