diff options
Diffstat (limited to 'src/handlers.rs')
| -rw-r--r-- | src/handlers.rs | 157 |
1 files changed, 73 insertions, 84 deletions
diff --git a/src/handlers.rs b/src/handlers.rs index a49d29a..b048681 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,57 +1,85 @@ use axum::{ extract::{Path, Query, State}, - http::StatusCode, + http::{HeaderName, StatusCode, header}, response::Json, }; use chrono::NaiveTime; use sqlx::PgPool; +use strum::IntoEnumIterator; use uuid::Uuid; -use crate::models::{AllQuery, DateQuery, MeasurementData, MeasurementView, Unit}; -use crate::{error::AppError, models::MetricType}; +use crate::error::AppError; +use crate::models::{ + ListQuery, MeasurementData, MeasurementView, MetricType, MetricUnitPair, Order, Unit, +}; -pub async fn index() -> &'static str { - "Hello World!" +pub async fn health() -> StatusCode { + StatusCode::OK } -pub async fn all( +pub async fn list_measurements( State(pool): State<PgPool>, - Query(options): Query<AllQuery>, + Query(query): Query<ListQuery>, ) -> Result<Json<Vec<MeasurementView>>, AppError> { - let limit = match options.limit { + let limit = match query.limit { Some(x) if (x > 0) => Some(x), None => None, _ => return Err(AppError::BadRequest), }; + let (from, to) = match (query.date, query.from, query.to) { + (Some(date), None, None) => (Some(date), Some(date)), + (None, from, to) => (from, to), + _ => 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, + }; // 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 + // `order` is desc (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, + WHERE ($1::timestamptz IS NULL OR measured_at >= $1) + AND ($2::timestamptz IS NULL OR measured_at < $2) + ORDER BY CASE WHEN $4 THEN measured_at END DESC, measured_at ASC - LIMIT $1 + LIMIT $3 ", + start, + end, limit, - options.reverse.unwrap_or(false) + matches!(query.order, Order::Desc) ) .fetch_all(&pool) .await?; Ok(Json(rows)) } -pub async fn add( +pub async fn create_measurement( State(pool): State<PgPool>, Json(data): Json<MeasurementData>, -) -> Result<(StatusCode, Json<Uuid>), AppError> { - let rec = sqlx::query!( +) -> Result<(StatusCode, [(HeaderName, String); 1], Json<MeasurementView>), AppError> { + let measurement = sqlx::query_as!( + MeasurementView, " INSERT INTO measurement (id, measured_at, metric_type, value, unit) VALUES ($1, $2, $3, $4, $5) - RETURNING id + RETURNING id, measured_at, metric_type, value, unit ", Uuid::now_v7(), data.measured_at, @@ -61,66 +89,15 @@ pub async fn add( ) .fetch_one(&pool) .await?; - Ok((StatusCode::CREATED, Json(rec.id))) -} - -pub async fn search( - State(pool): State<PgPool>, - Query(date_query): Query<DateQuery>, -) -> Result<Json<Vec<MeasurementView>>, 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<PgPool>) -> Result<Json<Vec<Unit>>, AppError> { - Ok(Json( - sqlx::query_scalar("SELECT enum_range(NULL::unit)") - .fetch_one(&pool) - .await?, + let location = format!("/measurements/{}", measurement.id); + Ok(( + StatusCode::CREATED, + [(header::LOCATION, location)], + Json(measurement), )) } -pub async fn metric_types(State(pool): State<PgPool>) -> Result<Json<Vec<MetricType>>, AppError> { - Ok(Json( - sqlx::query_scalar("SELECT enum_range(NULL::metric_type)") - .fetch_one(&pool) - .await?, - )) -} - -pub async fn measurement_by_id( +pub async fn get_measurement( State(pool): State<PgPool>, Path(id): Path<Uuid>, ) -> Result<Json<MeasurementView>, AppError> { @@ -137,17 +114,7 @@ pub async fn measurement_by_id( Ok(Json(measurement)) } -pub async fn delete( - State(pool): State<PgPool>, - Path(id): Path<Uuid>, -) -> Result<StatusCode, AppError> { - sqlx::query!("DELETE FROM measurement WHERE id = $1", id) - .execute(&pool) - .await?; - Ok(StatusCode::NO_CONTENT) -} - -pub async fn update( +pub async fn update_measurement( State(pool): State<PgPool>, Path(id): Path<Uuid>, Json(data): Json<MeasurementData>, @@ -174,3 +141,25 @@ pub async fn update( } Ok(StatusCode::NO_CONTENT) } + +pub async fn delete_measurement( + State(pool): State<PgPool>, + Path(id): Path<Uuid>, +) -> Result<StatusCode, AppError> { + sqlx::query!("DELETE FROM measurement WHERE id = $1", id) + .execute(&pool) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +pub async fn metric_types() -> Json<Vec<MetricUnitPair>> { + Json( + MetricType::iter() + .flat_map(|metric_type| { + Unit::iter() + .filter(move |unit| metric_type.accepts(unit)) + .map(move |unit| MetricUnitPair { metric_type, unit }) + }) + .collect(), + ) +} |
