feat: add optional created_at to entity creation and updates
Add `created_at: Option<DateTime<Utc>>` through all layers so CLI users can backdate entities at creation/update time. When omitted, falls back to `Utc::now()`. - Domain: add field to all New*/Update* structs with serde(default) - Domain: timeline events use entity created_at instead of Utc::now() - Repos: unify INSERT to explicit Rust-side created_at with unwrap_or_else - Repos: add created_at to UPDATE dynamic query builders - Routes: add field to submission structs and has_changes guards - Clients: pass created_at through manual JSON client methods - CLI: add --created-at flag with parse_created_at helper (RFC 3339 or YYYY-MM-DD)
This commit is contained in:
parent
e58ea5758f
commit
e01b6d0a1c
33 changed files with 250 additions and 28 deletions
|
|
@ -2,6 +2,7 @@ use axum::Json;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|
@ -125,6 +126,7 @@ pub(crate) async fn update_bag(
|
||||||
remaining: None,
|
remaining: None,
|
||||||
closed: None,
|
closed: None,
|
||||||
finished_at: None,
|
finished_at: None,
|
||||||
|
created_at: None,
|
||||||
},
|
},
|
||||||
|Json(p)| p,
|
|Json(p)| p,
|
||||||
);
|
);
|
||||||
|
|
@ -133,6 +135,7 @@ pub(crate) async fn update_bag(
|
||||||
remaining: body_update.remaining.or(update_params.remaining),
|
remaining: body_update.remaining.or(update_params.remaining),
|
||||||
closed: body_update.closed.or(update_params.closed),
|
closed: body_update.closed.or(update_params.closed),
|
||||||
finished_at: body_update.finished_at.or(update_params.finished_at),
|
finished_at: body_update.finished_at.or(update_params.finished_at),
|
||||||
|
created_at: body_update.created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bag = if let Some(true) = update.closed {
|
let bag = if let Some(true) = update.closed {
|
||||||
|
|
@ -183,6 +186,8 @@ pub(crate) struct NewBagSubmission {
|
||||||
roast_id: RoastId,
|
roast_id: RoastId,
|
||||||
roast_date: Option<String>,
|
roast_date: Option<String>,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewBagSubmission {
|
impl NewBagSubmission {
|
||||||
|
|
@ -208,6 +213,7 @@ impl NewBagSubmission {
|
||||||
roast_id,
|
roast_id,
|
||||||
roast_date,
|
roast_date,
|
||||||
amount: self.amount,
|
amount: self.amount,
|
||||||
|
created_at: self.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use axum::Json;
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|
@ -188,6 +189,8 @@ pub(crate) struct NewBrewSubmission {
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
#[serde(default, deserialize_with = "deserialize_quick_notes")]
|
#[serde(default, deserialize_with = "deserialize_quick_notes")]
|
||||||
quick_notes: Vec<QuickNote>,
|
quick_notes: Vec<QuickNote>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewBrewSubmission {
|
impl NewBrewSubmission {
|
||||||
|
|
@ -217,6 +220,7 @@ impl NewBrewSubmission {
|
||||||
water_volume: self.water_volume,
|
water_volume: self.water_volume,
|
||||||
water_temp: self.water_temp,
|
water_temp: self.water_temp,
|
||||||
quick_notes: self.quick_notes,
|
quick_notes: self.quick_notes,
|
||||||
|
created_at: self.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ pub(crate) async fn update_cafe(
|
||||||
|| payload.country.is_some()
|
|| payload.country.is_some()
|
||||||
|| payload.latitude.is_some()
|
|| payload.latitude.is_some()
|
||||||
|| payload.longitude.is_some()
|
|| payload.longitude.is_some()
|
||||||
|| payload.website.is_some();
|
|| payload.website.is_some()
|
||||||
|
|| payload.created_at.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ pub(crate) async fn submit_checkin(
|
||||||
latitude: submission.cafe_lat,
|
latitude: submission.cafe_lat,
|
||||||
longitude: submission.cafe_lng,
|
longitude: submission.cafe_lng,
|
||||||
website: submission.cafe_website.filter(|s| !s.is_empty()),
|
website: submission.cafe_website.filter(|s| !s.is_empty()),
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
.normalize();
|
.normalize();
|
||||||
|
|
||||||
|
|
@ -79,6 +80,7 @@ pub(crate) async fn submit_checkin(
|
||||||
let new_cup = NewCup {
|
let new_cup = NewCup {
|
||||||
roast_id: RoastId::from(roast_id),
|
roast_id: RoastId::from(roast_id),
|
||||||
cafe_id,
|
cafe_id,
|
||||||
|
created_at: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cup = state
|
let cup = state
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use axum::Json;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|
@ -148,6 +149,8 @@ pub(crate) struct NewGearSubmission {
|
||||||
category: String,
|
category: String,
|
||||||
make: String,
|
make: String,
|
||||||
model: String,
|
model: String,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewGearSubmission {
|
impl NewGearSubmission {
|
||||||
|
|
@ -167,6 +170,7 @@ impl NewGearSubmission {
|
||||||
category,
|
category,
|
||||||
make: self.make,
|
make: self.make,
|
||||||
model: self.model,
|
model: self.model,
|
||||||
|
created_at: self.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ pub(crate) async fn update_roaster(
|
||||||
let has_changes = payload.name.is_some()
|
let has_changes = payload.name.is_some()
|
||||||
|| payload.country.is_some()
|
|| payload.country.is_some()
|
||||||
|| payload.city.is_some()
|
|| payload.city.is_some()
|
||||||
|| payload.homepage.is_some();
|
|| payload.homepage.is_some()
|
||||||
|
|| payload.created_at.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use axum::Json;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::macros::{
|
use super::macros::{
|
||||||
|
|
@ -160,7 +161,8 @@ pub(crate) async fn update_roast(
|
||||||
|| payload.region.is_some()
|
|| payload.region.is_some()
|
||||||
|| payload.producer.is_some()
|
|| payload.producer.is_some()
|
||||||
|| payload.tasting_notes.is_some()
|
|| payload.tasting_notes.is_some()
|
||||||
|| payload.process.is_some();
|
|| payload.process.is_some()
|
||||||
|
|| payload.created_at.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
@ -197,6 +199,8 @@ pub(crate) struct NewRoastSubmission {
|
||||||
producer: String,
|
producer: String,
|
||||||
tasting_notes: TastingNotesInput,
|
tasting_notes: TastingNotesInput,
|
||||||
process: String,
|
process: String,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewRoastSubmission {
|
impl NewRoastSubmission {
|
||||||
|
|
@ -234,6 +238,7 @@ impl NewRoastSubmission {
|
||||||
producer,
|
producer,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
process,
|
process,
|
||||||
|
created_at: self.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ async fn match_existing_entities(
|
||||||
country: roaster_country.to_string(),
|
country: roaster_country.to_string(),
|
||||||
city: result.roaster.city.clone(),
|
city: result.roaster.city.clone(),
|
||||||
homepage: None,
|
homepage: None,
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
.normalize();
|
.normalize();
|
||||||
let roaster_slug = temp_roaster.slug();
|
let roaster_slug = temp_roaster.slug();
|
||||||
|
|
@ -281,6 +282,7 @@ pub(crate) async fn submit_scan(
|
||||||
country: submission.roaster_country,
|
country: submission.roaster_country,
|
||||||
city: submission.roaster_city,
|
city: submission.roaster_city,
|
||||||
homepage: submission.roaster_homepage,
|
homepage: submission.roaster_homepage,
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
.normalize();
|
.normalize();
|
||||||
|
|
||||||
|
|
@ -317,6 +319,7 @@ pub(crate) async fn submit_scan(
|
||||||
producer: submission.producer.trim().to_string(),
|
producer: submission.producer.trim().to_string(),
|
||||||
process: submission.process.trim().to_string(),
|
process: submission.process.trim().to_string(),
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fn require(field: &str, value: &str) -> Result<String, AppError> {
|
fn require(field: &str, value: &str) -> Result<String, AppError> {
|
||||||
|
|
@ -336,6 +339,7 @@ pub(crate) async fn submit_scan(
|
||||||
producer: require("producer", &submission.producer)?,
|
producer: require("producer", &submission.producer)?,
|
||||||
process: require("process", &submission.process)?,
|
process: require("process", &submission.process)?,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -358,6 +362,7 @@ pub(crate) async fn submit_scan(
|
||||||
roast_id: roast.id,
|
roast_id: roast.id,
|
||||||
roast_date: None,
|
roast_date: None,
|
||||||
amount,
|
amount,
|
||||||
|
created_at: None,
|
||||||
};
|
};
|
||||||
state
|
state
|
||||||
.bag_service
|
.bag_service
|
||||||
|
|
@ -416,6 +421,7 @@ async fn submit_existing_roast(
|
||||||
roast_id: roast.id,
|
roast_id: roast.id,
|
||||||
roast_date: None,
|
roast_date: None,
|
||||||
amount,
|
amount,
|
||||||
|
created_at: None,
|
||||||
};
|
};
|
||||||
state
|
state
|
||||||
.bag_service
|
.bag_service
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ pub struct NewBag {
|
||||||
pub roast_id: RoastId,
|
pub roast_id: RoastId,
|
||||||
pub roast_date: Option<NaiveDate>,
|
pub roast_date: Option<NaiveDate>,
|
||||||
pub amount: f64,
|
pub amount: f64,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -42,6 +44,8 @@ pub struct UpdateBag {
|
||||||
pub remaining: Option<f64>,
|
pub remaining: Option<f64>,
|
||||||
pub closed: Option<bool>,
|
pub closed: Option<bool>,
|
||||||
pub finished_at: Option<NaiveDate>,
|
pub finished_at: Option<NaiveDate>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter criteria for bag queries.
|
/// Filter criteria for bag queries.
|
||||||
|
|
@ -141,7 +145,7 @@ pub fn bag_timeline_event(
|
||||||
entity_type: "bag".to_string(),
|
entity_type: "bag".to_string(),
|
||||||
entity_id: bag.id.into_inner(),
|
entity_id: bag.id.into_inner(),
|
||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: bag.created_at,
|
||||||
title: roast.name.clone(),
|
title: roast.name.clone(),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ impl BrewWithDetails {
|
||||||
entity_type: "brew".to_string(),
|
entity_type: "brew".to_string(),
|
||||||
entity_id: self.brew.id.into_inner(),
|
entity_id: self.brew.id.into_inner(),
|
||||||
action: "brewed".to_string(),
|
action: "brewed".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: self.brew.created_at,
|
||||||
title: self.roast_name.clone(),
|
title: self.roast_name.clone(),
|
||||||
details,
|
details,
|
||||||
tasting_notes: vec![],
|
tasting_notes: vec![],
|
||||||
|
|
@ -187,6 +187,8 @@ pub struct NewBrew {
|
||||||
pub water_volume: i32,
|
pub water_volume: i32,
|
||||||
pub water_temp: f64,
|
pub water_temp: f64,
|
||||||
pub quick_notes: Vec<QuickNote>,
|
pub quick_notes: Vec<QuickNote>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter criteria for brew queries.
|
/// Filter criteria for brew queries.
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ impl Cafe {
|
||||||
entity_type: "cafe".to_string(),
|
entity_type: "cafe".to_string(),
|
||||||
entity_id: self.id.into_inner(),
|
entity_id: self.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: self.created_at,
|
||||||
title: self.name.clone(),
|
title: self.name.clone(),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
|
|
@ -53,6 +53,8 @@ pub struct NewCafe {
|
||||||
pub latitude: f64,
|
pub latitude: f64,
|
||||||
pub longitude: f64,
|
pub longitude: f64,
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewCafe {
|
impl NewCafe {
|
||||||
|
|
@ -88,6 +90,8 @@ pub struct UpdateCafe {
|
||||||
pub latitude: Option<f64>,
|
pub latitude: Option<f64>,
|
||||||
pub longitude: Option<f64>,
|
pub longitude: Option<f64>,
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ impl CupWithDetails {
|
||||||
entity_type: "cup".to_string(),
|
entity_type: "cup".to_string(),
|
||||||
entity_id: self.cup.id.into_inner(),
|
entity_id: self.cup.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: self.cup.created_at,
|
||||||
title: self.roast_name.clone(),
|
title: self.roast_name.clone(),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
|
|
@ -61,6 +61,8 @@ impl CupWithDetails {
|
||||||
pub struct NewCup {
|
pub struct NewCup {
|
||||||
pub roast_id: RoastId,
|
pub roast_id: RoastId,
|
||||||
pub cafe_id: CafeId,
|
pub cafe_id: CafeId,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter criteria for cup queries.
|
/// Filter criteria for cup queries.
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ impl Gear {
|
||||||
entity_type: "gear".to_string(),
|
entity_type: "gear".to_string(),
|
||||||
entity_id: self.id.into_inner(),
|
entity_id: self.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: self.created_at,
|
||||||
title: format!("{} {}", self.make, self.model),
|
title: format!("{} {}", self.make, self.model),
|
||||||
details: vec![
|
details: vec![
|
||||||
TimelineEventDetail {
|
TimelineEventDetail {
|
||||||
|
|
@ -92,12 +92,16 @@ pub struct NewGear {
|
||||||
pub category: GearCategory,
|
pub category: GearCategory,
|
||||||
pub make: String,
|
pub make: String,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UpdateGear {
|
pub struct UpdateGear {
|
||||||
pub make: Option<String>,
|
pub make: Option<String>,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ pub struct NewRoaster {
|
||||||
pub country: String,
|
pub country: String,
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewRoaster {
|
impl NewRoaster {
|
||||||
|
|
@ -77,7 +79,7 @@ impl Roaster {
|
||||||
entity_type: "roaster".to_string(),
|
entity_type: "roaster".to_string(),
|
||||||
entity_id: self.id.into_inner(),
|
entity_id: self.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: self.created_at,
|
||||||
title: self.name.clone(),
|
title: self.name.clone(),
|
||||||
details,
|
details,
|
||||||
tasting_notes: vec![],
|
tasting_notes: vec![],
|
||||||
|
|
@ -94,6 +96,8 @@ pub struct UpdateRoaster {
|
||||||
pub country: Option<String>,
|
pub country: Option<String>,
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateRoaster {
|
impl UpdateRoaster {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ pub struct NewRoast {
|
||||||
pub producer: String,
|
pub producer: String,
|
||||||
pub tasting_notes: Vec<String>,
|
pub tasting_notes: Vec<String>,
|
||||||
pub process: String,
|
pub process: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewRoast {
|
impl NewRoast {
|
||||||
|
|
@ -54,6 +56,8 @@ pub struct UpdateRoast {
|
||||||
pub producer: Option<String>,
|
pub producer: Option<String>,
|
||||||
pub tasting_notes: Option<Vec<String>>,
|
pub tasting_notes: Option<Vec<String>>,
|
||||||
pub process: Option<String>,
|
pub process: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
|
@ -120,7 +124,7 @@ pub fn roast_timeline_event(roast: &Roast, roaster: &Roaster) -> NewTimelineEven
|
||||||
entity_type: "roast".to_string(),
|
entity_type: "roast".to_string(),
|
||||||
entity_id: roast.id.into_inner(),
|
entity_id: roast.id.into_inner(),
|
||||||
action: "added".to_string(),
|
action: "added".to_string(),
|
||||||
occurred_at: Utc::now(),
|
occurred_at: roast.created_at,
|
||||||
title: roast.name.clone(),
|
title: roast.name.clone(),
|
||||||
details,
|
details,
|
||||||
tasting_notes: roast.tasting_notes.clone(),
|
tasting_notes: roast.tasting_notes.clone(),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use chrono::NaiveDate;
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
|
|
||||||
use crate::domain::bags::{BagWithRoast, UpdateBag};
|
use crate::domain::bags::{BagWithRoast, UpdateBag};
|
||||||
use crate::domain::ids::{BagId, RoastId};
|
use crate::domain::ids::{BagId, RoastId};
|
||||||
|
|
@ -20,13 +20,17 @@ impl<'a> BagsClient<'a> {
|
||||||
roast_id: RoastId,
|
roast_id: RoastId,
|
||||||
roast_date: Option<NaiveDate>,
|
roast_date: Option<NaiveDate>,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
) -> Result<BagWithRoast> {
|
) -> Result<BagWithRoast> {
|
||||||
let url = self.inner.endpoint("api/v1/bags")?;
|
let url = self.inner.endpoint("api/v1/bags")?;
|
||||||
let payload = serde_json::json!({
|
let mut payload = serde_json::json!({
|
||||||
"roast_id": roast_id,
|
"roast_id": roast_id,
|
||||||
"roast_date": roast_date.map(|d| d.to_string()),
|
"roast_date": roast_date.map(|d| d.to_string()),
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
});
|
});
|
||||||
|
if let Some(ts) = created_at {
|
||||||
|
payload["created_at"] = serde_json::json!(ts);
|
||||||
|
}
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.inner
|
.inner
|
||||||
|
|
@ -74,12 +78,14 @@ impl<'a> BagsClient<'a> {
|
||||||
remaining: Option<f64>,
|
remaining: Option<f64>,
|
||||||
closed: Option<bool>,
|
closed: Option<bool>,
|
||||||
finished_at: Option<NaiveDate>,
|
finished_at: Option<NaiveDate>,
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
) -> Result<BagWithRoast> {
|
) -> Result<BagWithRoast> {
|
||||||
let url = self.inner.endpoint(&format!("api/v1/bags/{id}"))?;
|
let url = self.inner.endpoint(&format!("api/v1/bags/{id}"))?;
|
||||||
let payload = UpdateBag {
|
let payload = UpdateBag {
|
||||||
remaining,
|
remaining,
|
||||||
closed,
|
closed,
|
||||||
finished_at,
|
finished_at,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::domain::brews::{BrewWithDetails, QuickNote};
|
use crate::domain::brews::{BrewWithDetails, QuickNote};
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
|
|
@ -26,6 +27,7 @@ impl<'a> BrewsClient<'a> {
|
||||||
water_volume: i32,
|
water_volume: i32,
|
||||||
water_temp: f64,
|
water_temp: f64,
|
||||||
quick_notes: Vec<QuickNote>,
|
quick_notes: Vec<QuickNote>,
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
) -> Result<BrewWithDetails> {
|
) -> Result<BrewWithDetails> {
|
||||||
let url = self.inner.endpoint("api/v1/brews")?;
|
let url = self.inner.endpoint("api/v1/brews")?;
|
||||||
let mut payload = serde_json::json!({
|
let mut payload = serde_json::json!({
|
||||||
|
|
@ -44,6 +46,9 @@ impl<'a> BrewsClient<'a> {
|
||||||
let labels: Vec<&str> = quick_notes.iter().map(|n| n.label()).collect();
|
let labels: Vec<&str> = quick_notes.iter().map(|n| n.label()).collect();
|
||||||
payload["quick_notes"] = serde_json::json!(labels);
|
payload["quick_notes"] = serde_json::json!(labels);
|
||||||
}
|
}
|
||||||
|
if let Some(ts) = created_at {
|
||||||
|
payload["created_at"] = serde_json::json!(ts);
|
||||||
|
}
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.inner
|
.inner
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::domain::gear::{Gear, UpdateGear};
|
use crate::domain::gear::{Gear, UpdateGear};
|
||||||
use crate::domain::ids::GearId;
|
use crate::domain::ids::GearId;
|
||||||
|
|
@ -14,13 +15,22 @@ impl<'a> GearClient<'a> {
|
||||||
Self { inner }
|
Self { inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(&self, category: &str, make: String, model: String) -> Result<Gear> {
|
pub async fn create(
|
||||||
|
&self,
|
||||||
|
category: &str,
|
||||||
|
make: String,
|
||||||
|
model: String,
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
|
) -> Result<Gear> {
|
||||||
let url = self.inner.endpoint("api/v1/gear")?;
|
let url = self.inner.endpoint("api/v1/gear")?;
|
||||||
let payload = serde_json::json!({
|
let mut payload = serde_json::json!({
|
||||||
"category": category,
|
"category": category,
|
||||||
"make": make,
|
"make": make,
|
||||||
"model": model,
|
"model": model,
|
||||||
});
|
});
|
||||||
|
if let Some(ts) = created_at {
|
||||||
|
payload["created_at"] = serde_json::json!(ts);
|
||||||
|
}
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.inner
|
.inner
|
||||||
|
|
@ -66,9 +76,14 @@ impl<'a> GearClient<'a> {
|
||||||
id: GearId,
|
id: GearId,
|
||||||
make: Option<String>,
|
make: Option<String>,
|
||||||
model: Option<String>,
|
model: Option<String>,
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
) -> Result<Gear> {
|
) -> Result<Gear> {
|
||||||
let url = self.inner.endpoint(&format!("api/v1/gear/{id}"))?;
|
let url = self.inner.endpoint(&format!("api/v1/gear/{id}"))?;
|
||||||
let payload = UpdateGear { make, model };
|
let payload = UpdateGear {
|
||||||
|
make,
|
||||||
|
model,
|
||||||
|
created_at,
|
||||||
|
};
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.inner
|
.inner
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ impl SqlBagRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl BagRepository for SqlBagRepository {
|
impl BagRepository for SqlBagRepository {
|
||||||
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError> {
|
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError> {
|
||||||
|
let created_at = bag.created_at.unwrap_or_else(Utc::now);
|
||||||
let query = r"
|
let query = r"
|
||||||
INSERT INTO bags (roast_id, roast_date, amount, remaining)
|
INSERT INTO bags (roast_id, roast_date, amount, remaining, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
RETURNING id, roast_id, roast_date, amount, remaining, closed, finished_at, created_at, updated_at
|
||||||
";
|
";
|
||||||
|
|
||||||
|
|
@ -123,6 +124,8 @@ impl BagRepository for SqlBagRepository {
|
||||||
.bind(bag.roast_date)
|
.bind(bag.roast_date)
|
||||||
.bind(bag.amount)
|
.bind(bag.amount)
|
||||||
.bind(bag.amount) // remaining starts as amount
|
.bind(bag.amount) // remaining starts as amount
|
||||||
|
.bind(created_at)
|
||||||
|
.bind(created_at)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
@ -206,6 +209,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
push_update_field!(builder, sep, "remaining", changes.remaining);
|
push_update_field!(builder, sep, "remaining", changes.remaining);
|
||||||
push_update_field!(builder, sep, "closed", changes.closed);
|
push_update_field!(builder, sep, "closed", changes.closed);
|
||||||
push_update_field!(builder, sep, "finished_at", changes.finished_at);
|
push_update_field!(builder, sep, "finished_at", changes.finished_at);
|
||||||
|
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||||
let _ = sep; // Suppress unused_assignments warning from macro
|
let _ = sep; // Suppress unused_assignments warning from macro
|
||||||
|
|
||||||
builder.push(" WHERE id = ");
|
builder.push(" WHERE id = ");
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,10 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the brew
|
// Insert the brew
|
||||||
|
let created_at = brew.created_at.unwrap_or_else(Utc::now);
|
||||||
let insert_query = r"
|
let insert_query = r"
|
||||||
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes)
|
INSERT INTO brews (bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at
|
RETURNING id, bag_id, coffee_weight, grinder_id, grind_setting, brewer_id, filter_paper_id, water_volume, water_temp, quick_notes, created_at, updated_at
|
||||||
";
|
";
|
||||||
|
|
||||||
|
|
@ -182,6 +183,8 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
.bind(brew.water_volume)
|
.bind(brew.water_volume)
|
||||||
.bind(brew.water_temp)
|
.bind(brew.water_temp)
|
||||||
.bind(Self::encode_quick_notes(&brew.quick_notes))
|
.bind(Self::encode_quick_notes(&brew.quick_notes))
|
||||||
|
.bind(created_at)
|
||||||
|
.bind(created_at)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
async fn insert(&self, new_cafe: NewCafe) -> Result<Cafe, RepositoryError> {
|
async fn insert(&self, new_cafe: NewCafe) -> Result<Cafe, RepositoryError> {
|
||||||
let new_cafe = new_cafe.normalize();
|
let new_cafe = new_cafe.normalize();
|
||||||
let slug = new_cafe.slug();
|
let slug = new_cafe.slug();
|
||||||
let now = Utc::now();
|
let now = new_cafe.created_at.unwrap_or_else(Utc::now);
|
||||||
|
|
||||||
let record = query_as::<_, CafeRecord>(
|
let record = query_as::<_, CafeRecord>(
|
||||||
"INSERT INTO cafes (name, slug, city, country, latitude, longitude, website, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\
|
"INSERT INTO cafes (name, slug, city, country, latitude, longitude, website, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\
|
||||||
|
|
@ -163,6 +163,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
push_update_field!(builder, sep, "latitude", changes.latitude);
|
push_update_field!(builder, sep, "latitude", changes.latitude);
|
||||||
push_update_field!(builder, sep, "longitude", changes.longitude);
|
push_update_field!(builder, sep, "longitude", changes.longitude);
|
||||||
push_update_field!(builder, sep, "website", changes.website);
|
push_update_field!(builder, sep, "website", changes.website);
|
||||||
|
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||||
let _ = sep;
|
let _ = sep;
|
||||||
|
|
||||||
builder.push(" WHERE id = ");
|
builder.push(" WHERE id = ");
|
||||||
|
|
|
||||||
|
|
@ -107,12 +107,15 @@ impl SqlCupRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CupRepository for SqlCupRepository {
|
impl CupRepository for SqlCupRepository {
|
||||||
async fn insert(&self, new_cup: NewCup) -> Result<Cup, RepositoryError> {
|
async fn insert(&self, new_cup: NewCup) -> Result<Cup, RepositoryError> {
|
||||||
|
let created_at = new_cup.created_at.unwrap_or_else(Utc::now);
|
||||||
let record = query_as::<_, CupRecord>(
|
let record = query_as::<_, CupRecord>(
|
||||||
"INSERT INTO cups (roast_id, cafe_id) VALUES (?, ?) \
|
"INSERT INTO cups (roast_id, cafe_id, created_at, updated_at) VALUES (?, ?, ?, ?) \
|
||||||
RETURNING id, roast_id, cafe_id, created_at, updated_at",
|
RETURNING id, roast_id, cafe_id, created_at, updated_at",
|
||||||
)
|
)
|
||||||
.bind(new_cup.roast_id.into_inner())
|
.bind(new_cup.roast_id.into_inner())
|
||||||
.bind(new_cup.cafe_id.into_inner())
|
.bind(new_cup.cafe_id.into_inner())
|
||||||
|
.bind(created_at)
|
||||||
|
.bind(created_at)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,10 @@ impl SqlGearRepository {
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl GearRepository for SqlGearRepository {
|
impl GearRepository for SqlGearRepository {
|
||||||
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
|
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
|
||||||
|
let created_at = gear.created_at.unwrap_or_else(Utc::now);
|
||||||
let query = r"
|
let query = r"
|
||||||
INSERT INTO gear (category, make, model)
|
INSERT INTO gear (category, make, model, created_at, updated_at)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
RETURNING id, category, make, model, created_at, updated_at
|
RETURNING id, category, make, model, created_at, updated_at
|
||||||
";
|
";
|
||||||
|
|
||||||
|
|
@ -73,6 +74,8 @@ impl GearRepository for SqlGearRepository {
|
||||||
.bind(gear.category.as_str())
|
.bind(gear.category.as_str())
|
||||||
.bind(&gear.make)
|
.bind(&gear.make)
|
||||||
.bind(&gear.model)
|
.bind(&gear.model)
|
||||||
|
.bind(created_at)
|
||||||
|
.bind(created_at)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
@ -142,6 +145,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
|
|
||||||
push_update_field!(builder, sep, "make", changes.make);
|
push_update_field!(builder, sep, "make", changes.make);
|
||||||
push_update_field!(builder, sep, "model", changes.model);
|
push_update_field!(builder, sep, "model", changes.model);
|
||||||
|
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||||
let _ = sep; // Suppress unused_assignments warning
|
let _ = sep; // Suppress unused_assignments warning
|
||||||
|
|
||||||
builder.push(" WHERE id = ");
|
builder.push(" WHERE id = ");
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
async fn insert(&self, new_roaster: NewRoaster) -> Result<Roaster, RepositoryError> {
|
async fn insert(&self, new_roaster: NewRoaster) -> Result<Roaster, RepositoryError> {
|
||||||
let new_roaster = new_roaster.normalize();
|
let new_roaster = new_roaster.normalize();
|
||||||
let slug = new_roaster.slug();
|
let slug = new_roaster.slug();
|
||||||
let created_at = Utc::now();
|
let created_at = new_roaster.created_at.unwrap_or_else(Utc::now);
|
||||||
|
|
||||||
let record = query_as::<_, RoasterRecord>(
|
let record = query_as::<_, RoasterRecord>(
|
||||||
"INSERT INTO roasters (name, slug, country, city, homepage, created_at) VALUES (?, ?, ?, ?, ?, ?)\
|
"INSERT INTO roasters (name, slug, country, city, homepage, created_at) VALUES (?, ?, ?, ?, ?, ?)\
|
||||||
|
|
@ -157,6 +157,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
push_update_field!(builder, sep, "country", changes.country);
|
push_update_field!(builder, sep, "country", changes.country);
|
||||||
push_update_field!(builder, sep, "city", changes.city);
|
push_update_field!(builder, sep, "city", changes.city);
|
||||||
push_update_field!(builder, sep, "homepage", changes.homepage);
|
push_update_field!(builder, sep, "homepage", changes.homepage);
|
||||||
|
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||||
|
|
||||||
if !sep {
|
if !sep {
|
||||||
return Err(RepositoryError::unexpected(
|
return Err(RepositoryError::unexpected(
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
producer,
|
producer,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
process,
|
process,
|
||||||
|
created_at,
|
||||||
} = new_roast;
|
} = new_roast;
|
||||||
|
|
||||||
let origin_value = empty_to_none(origin);
|
let origin_value = empty_to_none(origin);
|
||||||
|
|
@ -74,7 +75,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
let producer_value = empty_to_none(producer);
|
let producer_value = empty_to_none(producer);
|
||||||
let process_value = empty_to_none(process);
|
let process_value = empty_to_none(process);
|
||||||
|
|
||||||
let created_at = Utc::now();
|
let created_at = created_at.unwrap_or_else(Utc::now);
|
||||||
let notes_json = Self::encode_notes(&tasting_notes)?;
|
let notes_json = Self::encode_notes(&tasting_notes)?;
|
||||||
|
|
||||||
let record = query_as::<_, RoastRecord>(
|
let record = query_as::<_, RoastRecord>(
|
||||||
|
|
@ -228,6 +229,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
push_update_field!(builder, sep, "region", changes.region);
|
push_update_field!(builder, sep, "region", changes.region);
|
||||||
push_update_field!(builder, sep, "producer", changes.producer);
|
push_update_field!(builder, sep, "producer", changes.producer);
|
||||||
push_update_field!(builder, sep, "process", changes.process);
|
push_update_field!(builder, sep, "process", changes.process);
|
||||||
|
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||||
|
|
||||||
// Handle tasting_notes specially due to JSON encoding
|
// Handle tasting_notes specially due to JSON encoding
|
||||||
if let Some(tasting_notes) = changes.tasting_notes {
|
if let Some(tasting_notes) = changes.tasting_notes {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::ids::{BagId, RoastId};
|
use crate::domain::ids::{BagId, RoastId};
|
||||||
use crate::infrastructure::client::BrewlogClient;
|
use crate::infrastructure::client::BrewlogClient;
|
||||||
|
|
@ -38,6 +39,9 @@ pub struct AddBagCommand {
|
||||||
pub roast_date: Option<String>,
|
pub roast_date: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub amount: f64,
|
pub amount: f64,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_bag(client: &BrewlogClient, command: AddBagCommand) -> Result<()> {
|
pub async fn add_bag(client: &BrewlogClient, command: AddBagCommand) -> Result<()> {
|
||||||
|
|
@ -45,9 +49,18 @@ pub async fn add_bag(client: &BrewlogClient, command: AddBagCommand) -> Result<(
|
||||||
.roast_date
|
.roast_date
|
||||||
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let bag = client
|
let bag = client
|
||||||
.bags()
|
.bags()
|
||||||
.create(RoastId::new(command.roast_id), roast_date, command.amount)
|
.create(
|
||||||
|
RoastId::new(command.roast_id),
|
||||||
|
roast_date,
|
||||||
|
command.amount,
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&bag)
|
print_json(&bag)
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +91,9 @@ pub struct UpdateBagCommand {
|
||||||
pub closed: Option<bool>,
|
pub closed: Option<bool>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub finished_at: Option<String>,
|
pub finished_at: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Result<()> {
|
pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Result<()> {
|
||||||
|
|
@ -85,6 +101,10 @@ pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Re
|
||||||
.finished_at
|
.finished_at
|
||||||
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let bag = client
|
let bag = client
|
||||||
.bags()
|
.bags()
|
||||||
|
|
@ -93,6 +113,7 @@ pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Re
|
||||||
command.remaining,
|
command.remaining,
|
||||||
command.closed,
|
command.closed,
|
||||||
finished_at,
|
finished_at,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&bag)
|
print_json(&bag)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::brews::QuickNote;
|
use crate::domain::brews::QuickNote;
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
|
|
@ -65,6 +66,10 @@ pub struct AddBrewCommand {
|
||||||
/// Quick notes (comma-separated: good,too-fast,too-slow,too-hot,under-extracted,over-extracted)
|
/// Quick notes (comma-separated: good,too-fast,too-slow,too-hot,under-extracted,over-extracted)
|
||||||
#[arg(long, value_delimiter = ',')]
|
#[arg(long, value_delimiter = ',')]
|
||||||
pub quick_notes: Vec<String>,
|
pub quick_notes: Vec<String>,
|
||||||
|
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result<()> {
|
pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result<()> {
|
||||||
|
|
@ -73,6 +78,10 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|s| QuickNote::from_str_value(s))
|
.filter_map(|s| QuickNote::from_str_value(s))
|
||||||
.collect();
|
.collect();
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let brew = client
|
let brew = client
|
||||||
.brews()
|
.brews()
|
||||||
|
|
@ -86,6 +95,7 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
||||||
command.water_volume,
|
command.water_volume,
|
||||||
command.water_temp,
|
command.water_temp,
|
||||||
quick_notes,
|
quick_notes,
|
||||||
|
created_at,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&brew)
|
print_json(&brew)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::cafes::{NewCafe, UpdateCafe};
|
use crate::domain::cafes::{NewCafe, UpdateCafe};
|
||||||
use crate::domain::ids::CafeId;
|
use crate::domain::ids::CafeId;
|
||||||
|
|
@ -45,9 +46,16 @@ pub struct AddCafeCommand {
|
||||||
pub longitude: f64,
|
pub longitude: f64,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_cafe(client: &BrewlogClient, command: AddCafeCommand) -> Result<()> {
|
pub async fn add_cafe(client: &BrewlogClient, command: AddCafeCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = NewCafe {
|
let payload = NewCafe {
|
||||||
name: command.name,
|
name: command.name,
|
||||||
city: command.city,
|
city: command.city,
|
||||||
|
|
@ -55,6 +63,7 @@ pub async fn add_cafe(client: &BrewlogClient, command: AddCafeCommand) -> Result
|
||||||
latitude: command.latitude,
|
latitude: command.latitude,
|
||||||
longitude: command.longitude,
|
longitude: command.longitude,
|
||||||
website: command.website,
|
website: command.website,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cafe = client.cafes().create(&payload).await?;
|
let cafe = client.cafes().create(&payload).await?;
|
||||||
|
|
@ -84,9 +93,16 @@ pub struct UpdateCafeCommand {
|
||||||
pub longitude: Option<f64>,
|
pub longitude: Option<f64>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub website: Option<String>,
|
pub website: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_cafe(client: &BrewlogClient, command: UpdateCafeCommand) -> Result<()> {
|
pub async fn update_cafe(client: &BrewlogClient, command: UpdateCafeCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = UpdateCafe {
|
let payload = UpdateCafe {
|
||||||
name: command.name,
|
name: command.name,
|
||||||
city: command.city,
|
city: command.city,
|
||||||
|
|
@ -94,6 +110,7 @@ pub async fn update_cafe(client: &BrewlogClient, command: UpdateCafeCommand) ->
|
||||||
latitude: command.latitude,
|
latitude: command.latitude,
|
||||||
longitude: command.longitude,
|
longitude: command.longitude,
|
||||||
website: command.website,
|
website: command.website,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cafe = client
|
let cafe = client
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::cups::NewCup;
|
use crate::domain::cups::NewCup;
|
||||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||||
|
|
@ -34,12 +35,20 @@ pub struct AddCupCommand {
|
||||||
pub roast_id: i64,
|
pub roast_id: i64,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub cafe_id: i64,
|
pub cafe_id: i64,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_cup(client: &BrewlogClient, command: AddCupCommand) -> Result<()> {
|
pub async fn add_cup(client: &BrewlogClient, command: AddCupCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = NewCup {
|
let payload = NewCup {
|
||||||
roast_id: RoastId::new(command.roast_id),
|
roast_id: RoastId::new(command.roast_id),
|
||||||
cafe_id: CafeId::new(command.cafe_id),
|
cafe_id: CafeId::new(command.cafe_id),
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cup = client.cups().create(&payload).await?;
|
let cup = client.cups().create(&payload).await?;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::ids::GearId;
|
use crate::domain::ids::GearId;
|
||||||
use crate::infrastructure::client::BrewlogClient;
|
use crate::infrastructure::client::BrewlogClient;
|
||||||
|
|
@ -38,12 +39,19 @@ pub struct AddGearCommand {
|
||||||
pub make: String,
|
pub make: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_gear(client: &BrewlogClient, command: AddGearCommand) -> Result<()> {
|
pub async fn add_gear(client: &BrewlogClient, command: AddGearCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let gear = client
|
let gear = client
|
||||||
.gear()
|
.gear()
|
||||||
.create(&command.category, command.make, command.model)
|
.create(&command.category, command.make, command.model, created_at)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&gear)
|
print_json(&gear)
|
||||||
}
|
}
|
||||||
|
|
@ -69,12 +77,24 @@ pub struct UpdateGearCommand {
|
||||||
pub make: Option<String>,
|
pub make: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_gear(client: &BrewlogClient, command: UpdateGearCommand) -> Result<()> {
|
pub async fn update_gear(client: &BrewlogClient, command: UpdateGearCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let gear = client
|
let gear = client
|
||||||
.gear()
|
.gear()
|
||||||
.update(GearId::new(command.id), command.make, command.model)
|
.update(
|
||||||
|
GearId::new(command.id),
|
||||||
|
command.make,
|
||||||
|
command.model,
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
print_json(&gear)
|
print_json(&gear)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ pub mod tokens;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
|
|
||||||
use backup::{BackupCommand, RestoreCommand};
|
use backup::{BackupCommand, RestoreCommand};
|
||||||
use bags::BagCommands;
|
use bags::BagCommands;
|
||||||
use brews::BrewCommands;
|
use brews::BrewCommands;
|
||||||
|
|
@ -129,6 +131,18 @@ pub struct ServeCommand {
|
||||||
pub foursquare_api_key: Option<String>,
|
pub foursquare_api_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_created_at(value: &str) -> anyhow::Result<DateTime<Utc>> {
|
||||||
|
if let Ok(dt) = DateTime::parse_from_rfc3339(value) {
|
||||||
|
return Ok(dt.with_timezone(&Utc));
|
||||||
|
}
|
||||||
|
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
|
||||||
|
return Ok(date.and_time(chrono::NaiveTime::MIN).and_utc());
|
||||||
|
}
|
||||||
|
anyhow::bail!(
|
||||||
|
"invalid date format: expected RFC 3339 (e.g. 2025-08-05T10:00:00Z) or YYYY-MM-DD"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn print_json<T>(value: &T) -> anyhow::Result<()>
|
pub(crate) fn print_json<T>(value: &T) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
T: serde::Serialize,
|
T: serde::Serialize,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::ids::RoasterId;
|
use crate::domain::ids::RoasterId;
|
||||||
use crate::domain::roasters::{NewRoaster, UpdateRoaster};
|
use crate::domain::roasters::{NewRoaster, UpdateRoaster};
|
||||||
|
|
@ -41,14 +42,22 @@ pub struct AddRoasterCommand {
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_roaster(client: &BrewlogClient, command: AddRoasterCommand) -> Result<()> {
|
pub async fn add_roaster(client: &BrewlogClient, command: AddRoasterCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = NewRoaster {
|
let payload = NewRoaster {
|
||||||
name: command.name,
|
name: command.name,
|
||||||
country: command.country,
|
country: command.country,
|
||||||
city: command.city,
|
city: command.city,
|
||||||
homepage: command.homepage,
|
homepage: command.homepage,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let roaster = client.roasters().create(&payload).await?;
|
let roaster = client.roasters().create(&payload).await?;
|
||||||
|
|
@ -74,14 +83,22 @@ pub struct UpdateRoasterCommand {
|
||||||
pub city: Option<String>,
|
pub city: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_roaster(client: &BrewlogClient, command: UpdateRoasterCommand) -> Result<()> {
|
pub async fn update_roaster(client: &BrewlogClient, command: UpdateRoasterCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = UpdateRoaster {
|
let payload = UpdateRoaster {
|
||||||
name: command.name,
|
name: command.name,
|
||||||
country: command.country,
|
country: command.country,
|
||||||
city: command.city,
|
city: command.city,
|
||||||
homepage: command.homepage,
|
homepage: command.homepage,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let roaster = client
|
let roaster = client
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
use super::macros::{define_delete_command, define_get_command};
|
use super::macros::{define_delete_command, define_get_command};
|
||||||
|
use super::parse_created_at;
|
||||||
use super::print_json;
|
use super::print_json;
|
||||||
use crate::domain::ids::{RoastId, RoasterId};
|
use crate::domain::ids::{RoastId, RoasterId};
|
||||||
use crate::domain::roasts::{NewRoast, UpdateRoast};
|
use crate::domain::roasts::{NewRoast, UpdateRoast};
|
||||||
|
|
@ -47,9 +48,16 @@ pub struct AddRoastCommand {
|
||||||
pub process: String,
|
pub process: String,
|
||||||
#[arg(long = "tasting-notes", required = true)]
|
#[arg(long = "tasting-notes", required = true)]
|
||||||
pub tasting_notes: Vec<String>,
|
pub tasting_notes: Vec<String>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_roast(client: &BrewlogClient, command: AddRoastCommand) -> Result<()> {
|
pub async fn add_roast(client: &BrewlogClient, command: AddRoastCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = NewRoast {
|
let payload = NewRoast {
|
||||||
roaster_id: RoasterId::new(command.roaster_id),
|
roaster_id: RoasterId::new(command.roaster_id),
|
||||||
name: command.name,
|
name: command.name,
|
||||||
|
|
@ -58,6 +66,7 @@ pub async fn add_roast(client: &BrewlogClient, command: AddRoastCommand) -> Resu
|
||||||
producer: command.producer,
|
producer: command.producer,
|
||||||
tasting_notes: command.tasting_notes,
|
tasting_notes: command.tasting_notes,
|
||||||
process: command.process,
|
process: command.process,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let roast = client.roasts().create(&payload).await?;
|
let roast = client.roasts().create(&payload).await?;
|
||||||
|
|
@ -98,9 +107,16 @@ pub struct UpdateRoastCommand {
|
||||||
pub process: Option<String>,
|
pub process: Option<String>,
|
||||||
#[arg(long = "tasting-notes")]
|
#[arg(long = "tasting-notes")]
|
||||||
pub tasting_notes: Option<Vec<String>>,
|
pub tasting_notes: Option<Vec<String>>,
|
||||||
|
/// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05)
|
||||||
|
#[arg(long)]
|
||||||
|
pub created_at: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_roast(client: &BrewlogClient, command: UpdateRoastCommand) -> Result<()> {
|
pub async fn update_roast(client: &BrewlogClient, command: UpdateRoastCommand) -> Result<()> {
|
||||||
|
let created_at = command
|
||||||
|
.created_at
|
||||||
|
.map(|s| parse_created_at(&s))
|
||||||
|
.transpose()?;
|
||||||
let payload = UpdateRoast {
|
let payload = UpdateRoast {
|
||||||
roaster_id: command.roaster_id.map(RoasterId::new),
|
roaster_id: command.roaster_id.map(RoasterId::new),
|
||||||
name: command.name,
|
name: command.name,
|
||||||
|
|
@ -109,6 +125,7 @@ pub async fn update_roast(client: &BrewlogClient, command: UpdateRoastCommand) -
|
||||||
producer: command.producer,
|
producer: command.producer,
|
||||||
tasting_notes: command.tasting_notes,
|
tasting_notes: command.tasting_notes,
|
||||||
process: command.process,
|
process: command.process,
|
||||||
|
created_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
let roast = client
|
let roast = client
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue