1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
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<PgPool>,
Query(options): Query<AllQuery>,
) -> Result<Json<Vec<MeasurementView>>, 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<PgPool>,
Json(data): Json<MeasurementData>,
) -> Result<(StatusCode, Json<Uuid>), 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<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?,
))
}
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(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<MeasurementView>, 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<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(
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
Json(data): Json<MeasurementData>,
) -> Result<StatusCode, AppError> {
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)
}
|