refactor(cups): remove rating field
Drop the rating column from cups. This removes the field from the domain model, repository, API routes (including the PUT/update endpoint), CLI, and all related tests.
This commit is contained in:
parent
33c27ed0d0
commit
6d12b71482
13 changed files with 36 additions and 319 deletions
|
|
@ -137,7 +137,7 @@ brewlog bag <cmd> Manage bags of coffee
|
|||
brewlog gear <cmd> Manage brewing gear (grinders, brewers, filter papers)
|
||||
brewlog brew <cmd> Manage brews (add, list, get, delete — no update)
|
||||
brewlog cafe <cmd> Manage cafes
|
||||
brewlog cup <cmd> Manage cups (cafe visits with ratings)
|
||||
brewlog cup <cmd> Manage cups (cafe visits)
|
||||
brewlog token <cmd> Manage API tokens (create, list, revoke)
|
||||
brewlog backup Export all data to JSON on stdout (requires BREWLOG_TOKEN)
|
||||
brewlog restore --file F Restore data from a JSON backup into an empty database (requires BREWLOG_TOKEN)
|
||||
|
|
|
|||
1
migrations/0020_remove_cup_rating.sql
Normal file
1
migrations/0020_remove_cup_rating.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE cups DROP COLUMN rating;
|
||||
|
|
@ -503,40 +503,33 @@ fi
|
|||
# Monmouth Coffee (London) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Red Brick Espresso") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kamwangi") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Monmouth Coffee") | .id')"
|
||||
|
||||
# Prufrock Coffee (London) - 3 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Kochere") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Laja") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Ben Saïd Natural") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Prufrock Coffee") | .id')"
|
||||
|
||||
# Hola Coffee (Madrid) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Cerro Azul") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Guji Highland") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')" \
|
||||
--rating 3
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Hola Coffee") | .id')"
|
||||
|
||||
# Toma Café (Madrid) - 1 cup
|
||||
./target/debug/brewlog cup add \
|
||||
|
|
@ -546,68 +539,56 @@ fi
|
|||
# The Barn (Berlin) - 3 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Gatomboya") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Los Pirineos") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Halo Beriti") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="The Barn") | .id')"
|
||||
|
||||
# Companion Coffee (Berlin) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="El Paraiso 92 Anaerobic") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Oreti SL28") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Companion Coffee") | .id')"
|
||||
|
||||
# Man Versus Machine (Munich) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Finca Tamana Washed") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Esmeralda Geisha") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Man Versus Machine") | .id')"
|
||||
|
||||
# Vits der Kaffee (Munich) - 1 cup
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="San Fermin") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Vits der Kaffee") | .id')" \
|
||||
--rating 3
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Vits der Kaffee") | .id')"
|
||||
|
||||
# Full Court Press (Bristol) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Suke Quto") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
|
||||
--rating 5
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Death to Decaf") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')" \
|
||||
--rating 3
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Full Court Press") | .id')"
|
||||
|
||||
# Small Street Espresso (Bristol) - 2 cups
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="Simbi") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')"
|
||||
|
||||
./target/debug/brewlog cup add \
|
||||
--roast-id "$(./target/debug/brewlog roast list | jq -r '.[] | select(.name=="La Chumeca") | .id')" \
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')" \
|
||||
--rating 4
|
||||
--cafe-id "$(./target/debug/brewlog cafe list | jq -r '.[] | select(.name=="Small Street Espresso") | .id')"
|
||||
|
||||
# ============================================================================
|
||||
# Bags - Various bags from different roasters with amounts ranging 100g-500g
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ pub(crate) struct CheckInSubmission {
|
|||
#[serde(default)]
|
||||
cafe_website: Option<String>,
|
||||
roast_id: String,
|
||||
#[serde(default)]
|
||||
rating: i32,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
|
|
@ -105,16 +103,9 @@ pub(crate) async fn submit_checkin(
|
|||
cafe.id
|
||||
};
|
||||
|
||||
let rating = if (1..=5).contains(&submission.rating) {
|
||||
Some(submission.rating)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: RoastId::from(roast_id),
|
||||
cafe_id,
|
||||
rating,
|
||||
};
|
||||
|
||||
let cup = state
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ use crate::application::routes::support::{
|
|||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
};
|
||||
use crate::application::server::AppState;
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup};
|
||||
use crate::domain::ids::CupId;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::presentation::web::templates::CupListTemplate;
|
||||
|
|
@ -54,12 +54,6 @@ pub(crate) async fn create_cup(
|
|||
let (request, search) = query.into_request_and_search::<CupSortKey>();
|
||||
let (new_cup, source) = payload.into_parts();
|
||||
|
||||
if let Some(rating) = new_cup.rating
|
||||
&& !(1..=5).contains(&rating)
|
||||
{
|
||||
return Err(AppError::validation("rating must be between 1 and 5").into());
|
||||
}
|
||||
|
||||
let cup = state
|
||||
.cup_repo
|
||||
.insert(new_cup)
|
||||
|
|
@ -94,33 +88,6 @@ pub(crate) async fn list_cups(
|
|||
|
||||
define_enriched_get_handler!(get_cup, CupId, CupWithDetails, cup_repo, get_with_details);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user))]
|
||||
pub(crate) async fn update_cup(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
Path(id): Path<CupId>,
|
||||
Json(payload): Json<UpdateCup>,
|
||||
) -> Result<Json<Cup>, ApiError> {
|
||||
let has_changes = payload.rating.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
}
|
||||
|
||||
if let Some(rating) = payload.rating.as_ref()
|
||||
&& !(1..=5).contains(rating)
|
||||
{
|
||||
return Err(AppError::validation("rating must be between 1 and 5").into());
|
||||
}
|
||||
|
||||
let cup = state
|
||||
.cup_repo
|
||||
.update(id, payload)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(cup))
|
||||
}
|
||||
|
||||
define_delete_handler!(
|
||||
delete_cup,
|
||||
CupId,
|
||||
|
|
|
|||
|
|
@ -91,12 +91,7 @@ pub fn app_router(state: AppState) -> axum::Router {
|
|||
.route("/scan", post(scan::submit_scan))
|
||||
.route("/check-in", post(checkin::submit_checkin))
|
||||
.route("/cups", get(cups::list_cups).post(cups::create_cup))
|
||||
.route(
|
||||
"/cups/:id",
|
||||
get(cups::get_cup)
|
||||
.put(cups::update_cup)
|
||||
.delete(cups::delete_cup),
|
||||
)
|
||||
.route("/cups/:id", get(cups::get_cup).delete(cups::delete_cup))
|
||||
.route(
|
||||
"/tokens",
|
||||
post(tokens::create_token).get(tokens::list_tokens),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ pub struct Cup {
|
|||
pub id: CupId,
|
||||
pub roast_id: RoastId,
|
||||
pub cafe_id: CafeId,
|
||||
pub rating: Option<i32>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -30,12 +29,6 @@ pub struct CupWithDetails {
|
|||
pub struct NewCup {
|
||||
pub roast_id: RoastId,
|
||||
pub cafe_id: CafeId,
|
||||
pub rating: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct UpdateCup {
|
||||
pub rating: Option<i32>,
|
||||
}
|
||||
|
||||
/// Filter criteria for cup queries.
|
||||
|
|
@ -73,7 +66,6 @@ pub enum CupSortKey {
|
|||
CreatedAt,
|
||||
CafeName,
|
||||
RoastName,
|
||||
Rating,
|
||||
}
|
||||
|
||||
impl SortKey for CupSortKey {
|
||||
|
|
@ -86,7 +78,6 @@ impl SortKey for CupSortKey {
|
|||
"created-at" => Some(CupSortKey::CreatedAt),
|
||||
"cafe" => Some(CupSortKey::CafeName),
|
||||
"roast" => Some(CupSortKey::RoastName),
|
||||
"rating" => Some(CupSortKey::Rating),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -96,13 +87,12 @@ impl SortKey for CupSortKey {
|
|||
CupSortKey::CreatedAt => "created-at",
|
||||
CupSortKey::CafeName => "cafe",
|
||||
CupSortKey::RoastName => "roast",
|
||||
CupSortKey::Rating => "rating",
|
||||
}
|
||||
}
|
||||
|
||||
fn default_direction(self) -> SortDirection {
|
||||
match self {
|
||||
CupSortKey::CreatedAt | CupSortKey::Rating => SortDirection::Desc,
|
||||
CupSortKey::CreatedAt => SortDirection::Desc,
|
||||
_ => SortDirection::Asc,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::domain::listing::{ListRequest, Page, SortDirection, SortKey};
|
|||
use crate::domain::bags::{Bag, BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
||||
use crate::domain::brews::{Brew, BrewFilter, BrewSortKey, BrewWithDetails, NewBrew};
|
||||
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup};
|
||||
use crate::domain::gear::{Gear, GearFilter, GearSortKey, NewGear, UpdateGear};
|
||||
use crate::domain::ids::{
|
||||
BagId, BrewId, CafeId, CupId, GearId, PasskeyCredentialId, RegistrationTokenId, RoastId,
|
||||
|
|
@ -222,7 +222,6 @@ pub trait CupRepository: Send + Sync {
|
|||
request: &ListRequest<CupSortKey>,
|
||||
search: Option<&str>,
|
||||
) -> Result<Page<CupWithDetails>, RepositoryError>;
|
||||
async fn update(&self, id: CupId, changes: UpdateCup) -> Result<Cup, RepositoryError>;
|
||||
async fn delete(&self, id: CupId) -> Result<(), RepositoryError>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::domain::cups::{Cup, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::cups::{Cup, CupWithDetails, NewCup};
|
||||
use crate::domain::ids::CupId;
|
||||
|
||||
use super::BrewlogClient;
|
||||
|
|
@ -52,19 +52,6 @@ impl<'a> CupsClient<'a> {
|
|||
self.inner.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn update(&self, id: CupId, payload: &UpdateCup) -> Result<Cup> {
|
||||
let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?;
|
||||
let response = self
|
||||
.inner
|
||||
.request(reqwest::Method::PUT, url)
|
||||
.json(payload)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to issue update cup request")?;
|
||||
|
||||
self.inner.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn delete(&self, id: CupId) -> Result<()> {
|
||||
let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?;
|
||||
let response = self
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{QueryBuilder, query, query_as};
|
||||
use sqlx::{query, query_as};
|
||||
|
||||
use super::macros::push_update_field;
|
||||
use crate::domain::RepositoryError;
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup};
|
||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||
use crate::domain::listing::{ListRequest, Page, SortDirection};
|
||||
use crate::domain::repositories::CupRepository;
|
||||
|
|
@ -13,7 +12,7 @@ use crate::infrastructure::database::DatabasePool;
|
|||
|
||||
const BASE_SELECT: &str = r"
|
||||
SELECT
|
||||
c.id, c.roast_id, c.cafe_id, c.rating,
|
||||
c.id, c.roast_id, c.cafe_id,
|
||||
c.created_at, c.updated_at,
|
||||
r.name as roast_name, r.slug as roast_slug,
|
||||
rr.name as roaster_name, rr.slug as roaster_slug,
|
||||
|
|
@ -48,7 +47,6 @@ impl SqlCupRepository {
|
|||
CupSortKey::RoastName => {
|
||||
format!("LOWER(r.name) {dir_sql}, c.created_at DESC")
|
||||
}
|
||||
CupSortKey::Rating => format!("c.rating {dir_sql}, c.created_at DESC"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +55,6 @@ impl SqlCupRepository {
|
|||
id: CupId::new(record.id),
|
||||
roast_id: RoastId::new(record.roast_id),
|
||||
cafe_id: CafeId::new(record.cafe_id),
|
||||
rating: record.rating,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
|
|
@ -69,7 +66,6 @@ impl SqlCupRepository {
|
|||
id: CupId::new(record.id),
|
||||
roast_id: RoastId::new(record.roast_id),
|
||||
cafe_id: CafeId::new(record.cafe_id),
|
||||
rating: record.rating,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
},
|
||||
|
|
@ -101,7 +97,7 @@ impl SqlCupRepository {
|
|||
}
|
||||
|
||||
fn details_for_cup(cup_with_details: &CupWithDetails) -> Result<String, RepositoryError> {
|
||||
let mut details = vec![
|
||||
let details = vec![
|
||||
TimelineEventDetail {
|
||||
label: "Coffee".to_string(),
|
||||
value: cup_with_details.roast_name.clone(),
|
||||
|
|
@ -116,13 +112,6 @@ impl SqlCupRepository {
|
|||
},
|
||||
];
|
||||
|
||||
if let Some(rating) = cup_with_details.cup.rating {
|
||||
details.push(TimelineEventDetail {
|
||||
label: "Rating".to_string(),
|
||||
value: format!("{rating}/5"),
|
||||
});
|
||||
}
|
||||
|
||||
serde_json::to_string(&details).map_err(|err| {
|
||||
RepositoryError::unexpected(format!("failed to encode timeline event details: {err}"))
|
||||
})
|
||||
|
|
@ -139,12 +128,11 @@ impl CupRepository for SqlCupRepository {
|
|||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||
|
||||
let record = query_as::<_, CupRecord>(
|
||||
"INSERT INTO cups (roast_id, cafe_id, rating) VALUES (?, ?, ?) \
|
||||
RETURNING id, roast_id, cafe_id, rating, created_at, updated_at",
|
||||
"INSERT INTO cups (roast_id, cafe_id) VALUES (?, ?) \
|
||||
RETURNING id, roast_id, cafe_id, created_at, updated_at",
|
||||
)
|
||||
.bind(new_cup.roast_id.into_inner())
|
||||
.bind(new_cup.cafe_id.into_inner())
|
||||
.bind(new_cup.rating)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||
|
|
@ -190,7 +178,7 @@ impl CupRepository for SqlCupRepository {
|
|||
|
||||
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
|
||||
let record = query_as::<_, CupRecord>(
|
||||
"SELECT id, roast_id, cafe_id, rating, created_at, updated_at FROM cups WHERE id = ?",
|
||||
"SELECT id, roast_id, cafe_id, created_at, updated_at FROM cups WHERE id = ?",
|
||||
)
|
||||
.bind(i64::from(id))
|
||||
.fetch_optional(&self.pool)
|
||||
|
|
@ -258,29 +246,6 @@ impl CupRepository for SqlCupRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn update(&self, id: CupId, changes: UpdateCup) -> Result<Cup, RepositoryError> {
|
||||
let mut builder = QueryBuilder::new("UPDATE cups SET updated_at = CURRENT_TIMESTAMP");
|
||||
let mut sep = true;
|
||||
|
||||
push_update_field!(builder, sep, "rating", changes.rating);
|
||||
let _ = sep;
|
||||
|
||||
builder.push(" WHERE id = ");
|
||||
builder.push_bind(i64::from(id));
|
||||
|
||||
let result = builder
|
||||
.build()
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(RepositoryError::NotFound);
|
||||
}
|
||||
|
||||
self.get(id).await
|
||||
}
|
||||
|
||||
async fn delete(&self, id: CupId) -> Result<(), RepositoryError> {
|
||||
let result = query("DELETE FROM cups WHERE id = ?")
|
||||
.bind(i64::from(id))
|
||||
|
|
@ -301,7 +266,6 @@ struct CupRecord {
|
|||
id: i64,
|
||||
roast_id: i64,
|
||||
cafe_id: i64,
|
||||
rating: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -311,7 +275,6 @@ struct CupWithDetailsRecord {
|
|||
id: i64,
|
||||
roast_id: i64,
|
||||
cafe_id: i64,
|
||||
rating: Option<i32>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
roast_name: String,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use clap::{Args, Subcommand};
|
|||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::print_json;
|
||||
use crate::domain::cups::{NewCup, UpdateCup};
|
||||
use crate::domain::cups::NewCup;
|
||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||
use crate::infrastructure::client::BrewlogClient;
|
||||
|
||||
|
|
@ -15,8 +15,6 @@ pub enum CupCommands {
|
|||
List,
|
||||
/// Get a cup by ID
|
||||
Get(GetCupCommand),
|
||||
/// Update a cup
|
||||
Update(UpdateCupCommand),
|
||||
/// Delete a cup
|
||||
Delete(DeleteCupCommand),
|
||||
}
|
||||
|
|
@ -26,7 +24,6 @@ pub async fn run(client: &BrewlogClient, cmd: CupCommands) -> Result<()> {
|
|||
CupCommands::Add(c) => add_cup(client, c).await,
|
||||
CupCommands::List => list_cups(client).await,
|
||||
CupCommands::Get(c) => get_cup(client, c).await,
|
||||
CupCommands::Update(c) => update_cup(client, c).await,
|
||||
CupCommands::Delete(c) => delete_cup(client, c).await,
|
||||
}
|
||||
}
|
||||
|
|
@ -37,15 +34,12 @@ pub struct AddCupCommand {
|
|||
pub roast_id: i64,
|
||||
#[arg(long)]
|
||||
pub cafe_id: i64,
|
||||
#[arg(long)]
|
||||
pub rating: Option<i32>,
|
||||
}
|
||||
|
||||
pub async fn add_cup(client: &BrewlogClient, command: AddCupCommand) -> Result<()> {
|
||||
let payload = NewCup {
|
||||
roast_id: RoastId::new(command.roast_id),
|
||||
cafe_id: CafeId::new(command.cafe_id),
|
||||
rating: command.rating,
|
||||
};
|
||||
|
||||
let cup = client.cups().create(&payload).await?;
|
||||
|
|
@ -58,25 +52,4 @@ pub async fn list_cups(client: &BrewlogClient) -> Result<()> {
|
|||
}
|
||||
|
||||
define_get_command!(GetCupCommand, get_cup, CupId, cups);
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct UpdateCupCommand {
|
||||
#[arg(long)]
|
||||
pub id: i64,
|
||||
#[arg(long)]
|
||||
pub rating: Option<i32>,
|
||||
}
|
||||
|
||||
pub async fn update_cup(client: &BrewlogClient, command: UpdateCupCommand) -> Result<()> {
|
||||
let payload = UpdateCup {
|
||||
rating: command.rating,
|
||||
};
|
||||
|
||||
let cup = client
|
||||
.cups()
|
||||
.update(CupId::new(command.id), &payload)
|
||||
.await?;
|
||||
print_json(&cup)
|
||||
}
|
||||
|
||||
define_delete_command!(DeleteCupCommand, delete_cup, CupId, cups, "cup");
|
||||
|
|
|
|||
|
|
@ -9,19 +9,11 @@ pub struct CupView {
|
|||
pub roaster_slug: String,
|
||||
pub cafe_name: String,
|
||||
pub cafe_slug: String,
|
||||
pub rating: String,
|
||||
pub has_rating: bool,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
impl CupView {
|
||||
pub fn from_domain(cup: CupWithDetails) -> Self {
|
||||
let rating = cup
|
||||
.cup
|
||||
.rating
|
||||
.map_or_else(|| "—".to_string(), |r| format!("{r}/5"));
|
||||
let has_rating = cup.cup.rating.is_some();
|
||||
|
||||
Self {
|
||||
id: cup.cup.id.to_string(),
|
||||
roast_name: cup.roast_name,
|
||||
|
|
@ -30,8 +22,6 @@ impl CupView {
|
|||
roaster_slug: cup.roaster_slug,
|
||||
cafe_name: cup.cafe_name,
|
||||
cafe_slug: cup.cafe_slug,
|
||||
rating,
|
||||
has_rating,
|
||||
created_at: cup.cup.created_at.format("%Y-%m-%d %H:%M").to_string(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::helpers::{
|
||||
create_default_cafe, create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||
};
|
||||
use brewlog::domain::cups::{Cup, CupWithDetails, NewCup, UpdateCup};
|
||||
use brewlog::domain::cups::{Cup, CupWithDetails, NewCup};
|
||||
use brewlog::domain::ids::{CafeId, RoastId};
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -16,7 +16,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
|
|||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: Some(5),
|
||||
};
|
||||
|
||||
let response = client
|
||||
|
|
@ -32,36 +31,6 @@ async fn creating_a_cup_returns_a_201_for_valid_data() {
|
|||
let cup: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(cup.roast_id, roast.id);
|
||||
assert_eq!(cup.cafe_id, cafe.id);
|
||||
assert_eq!(cup.rating, Some(5));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creating_a_cup_without_optional_fields_returns_a_201() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(app.api_url("/cups"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_cup)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 201);
|
||||
|
||||
let cup: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(cup.rating, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -72,7 +41,6 @@ async fn creating_a_cup_requires_authentication() {
|
|||
let new_cup = NewCup {
|
||||
roast_id: RoastId::new(1),
|
||||
cafe_id: CafeId::new(1),
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let response = client
|
||||
|
|
@ -114,7 +82,6 @@ async fn listing_cups_returns_a_200_with_enriched_data() {
|
|||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: Some(4),
|
||||
};
|
||||
|
||||
client
|
||||
|
|
@ -152,7 +119,6 @@ async fn getting_a_cup_returns_a_200_with_details() {
|
|||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: Some(3),
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
|
|
@ -196,91 +162,6 @@ async fn getting_a_nonexistent_cup_returns_a_404() {
|
|||
assert_eq!(response.status(), 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_cup_returns_a_200_for_valid_data() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/cups"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_cup)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create cup");
|
||||
|
||||
let cup: Cup = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update = UpdateCup { rating: Some(4) };
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cups/{}", cup.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let updated: Cup = response.json().await.expect("Failed to parse response");
|
||||
assert_eq!(updated.rating, Some(4));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn updating_a_cup_with_no_changes_returns_a_400() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let roaster = create_default_roaster(&app).await;
|
||||
let roast = create_default_roast(&app, roaster.id).await;
|
||||
let cafe = create_default_cafe(&app).await;
|
||||
|
||||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
.post(app.api_url("/cups"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&new_cup)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create cup");
|
||||
|
||||
let cup: Cup = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to parse response");
|
||||
|
||||
let update = UpdateCup { rating: None };
|
||||
|
||||
let response = client
|
||||
.put(app.api_url(&format!("/cups/{}", cup.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to execute request");
|
||||
|
||||
assert_eq!(response.status(), 400);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_a_cup_returns_a_204_for_valid_id() {
|
||||
let app = spawn_app_with_auth().await;
|
||||
|
|
@ -293,7 +174,6 @@ async fn deleting_a_cup_returns_a_204_for_valid_id() {
|
|||
let new_cup = NewCup {
|
||||
roast_id: roast.id,
|
||||
cafe_id: cafe.id,
|
||||
rating: None,
|
||||
};
|
||||
|
||||
let create_response = client
|
||||
|
|
|
|||
Loading…
Reference in a new issue