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::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
use tracing::info;
|
||||
|
||||
|
|
@ -125,6 +126,7 @@ pub(crate) async fn update_bag(
|
|||
remaining: None,
|
||||
closed: None,
|
||||
finished_at: None,
|
||||
created_at: None,
|
||||
},
|
||||
|Json(p)| p,
|
||||
);
|
||||
|
|
@ -133,6 +135,7 @@ pub(crate) async fn update_bag(
|
|||
remaining: body_update.remaining.or(update_params.remaining),
|
||||
closed: body_update.closed.or(update_params.closed),
|
||||
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 {
|
||||
|
|
@ -183,6 +186,8 @@ pub(crate) struct NewBagSubmission {
|
|||
roast_id: RoastId,
|
||||
roast_date: Option<String>,
|
||||
amount: f64,
|
||||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewBagSubmission {
|
||||
|
|
@ -208,6 +213,7 @@ impl NewBagSubmission {
|
|||
roast_id,
|
||||
roast_date,
|
||||
amount: self.amount,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use axum::Json;
|
|||
use axum::extract::{Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use tracing::info;
|
||||
|
||||
|
|
@ -188,6 +189,8 @@ pub(crate) struct NewBrewSubmission {
|
|||
water_temp: f64,
|
||||
#[serde(default, deserialize_with = "deserialize_quick_notes")]
|
||||
quick_notes: Vec<QuickNote>,
|
||||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewBrewSubmission {
|
||||
|
|
@ -217,6 +220,7 @@ impl NewBrewSubmission {
|
|||
water_volume: self.water_volume,
|
||||
water_temp: self.water_temp,
|
||||
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.latitude.is_some()
|
||||
|| payload.longitude.is_some()
|
||||
|| payload.website.is_some();
|
||||
|| payload.website.is_some()
|
||||
|| payload.created_at.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ pub(crate) async fn submit_checkin(
|
|||
latitude: submission.cafe_lat,
|
||||
longitude: submission.cafe_lng,
|
||||
website: submission.cafe_website.filter(|s| !s.is_empty()),
|
||||
created_at: None,
|
||||
}
|
||||
.normalize();
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ pub(crate) async fn submit_checkin(
|
|||
let new_cup = NewCup {
|
||||
roast_id: RoastId::from(roast_id),
|
||||
cafe_id,
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
let cup = state
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use axum::Json;
|
|||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
use tracing::info;
|
||||
|
||||
|
|
@ -148,6 +149,8 @@ pub(crate) struct NewGearSubmission {
|
|||
category: String,
|
||||
make: String,
|
||||
model: String,
|
||||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewGearSubmission {
|
||||
|
|
@ -167,6 +170,7 @@ impl NewGearSubmission {
|
|||
category,
|
||||
make: self.make,
|
||||
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()
|
||||
|| payload.country.is_some()
|
||||
|| payload.city.is_some()
|
||||
|| payload.homepage.is_some();
|
||||
|| payload.homepage.is_some()
|
||||
|| payload.created_at.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use axum::Json;
|
|||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::macros::{
|
||||
|
|
@ -160,7 +161,8 @@ pub(crate) async fn update_roast(
|
|||
|| payload.region.is_some()
|
||||
|| payload.producer.is_some()
|
||||
|| payload.tasting_notes.is_some()
|
||||
|| payload.process.is_some();
|
||||
|| payload.process.is_some()
|
||||
|| payload.created_at.is_some();
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::validation("no changes provided").into());
|
||||
|
|
@ -197,6 +199,8 @@ pub(crate) struct NewRoastSubmission {
|
|||
producer: String,
|
||||
tasting_notes: TastingNotesInput,
|
||||
process: String,
|
||||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewRoastSubmission {
|
||||
|
|
@ -234,6 +238,7 @@ impl NewRoastSubmission {
|
|||
producer,
|
||||
tasting_notes,
|
||||
process,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ async fn match_existing_entities(
|
|||
country: roaster_country.to_string(),
|
||||
city: result.roaster.city.clone(),
|
||||
homepage: None,
|
||||
created_at: None,
|
||||
}
|
||||
.normalize();
|
||||
let roaster_slug = temp_roaster.slug();
|
||||
|
|
@ -281,6 +282,7 @@ pub(crate) async fn submit_scan(
|
|||
country: submission.roaster_country,
|
||||
city: submission.roaster_city,
|
||||
homepage: submission.roaster_homepage,
|
||||
created_at: None,
|
||||
}
|
||||
.normalize();
|
||||
|
||||
|
|
@ -317,6 +319,7 @@ pub(crate) async fn submit_scan(
|
|||
producer: submission.producer.trim().to_string(),
|
||||
process: submission.process.trim().to_string(),
|
||||
tasting_notes,
|
||||
created_at: None,
|
||||
}
|
||||
} else {
|
||||
fn require(field: &str, value: &str) -> Result<String, AppError> {
|
||||
|
|
@ -336,6 +339,7 @@ pub(crate) async fn submit_scan(
|
|||
producer: require("producer", &submission.producer)?,
|
||||
process: require("process", &submission.process)?,
|
||||
tasting_notes,
|
||||
created_at: None,
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -358,6 +362,7 @@ pub(crate) async fn submit_scan(
|
|||
roast_id: roast.id,
|
||||
roast_date: None,
|
||||
amount,
|
||||
created_at: None,
|
||||
};
|
||||
state
|
||||
.bag_service
|
||||
|
|
@ -416,6 +421,7 @@ async fn submit_existing_roast(
|
|||
roast_id: roast.id,
|
||||
roast_date: None,
|
||||
amount,
|
||||
created_at: None,
|
||||
};
|
||||
state
|
||||
.bag_service
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ pub struct NewBag {
|
|||
pub roast_id: RoastId,
|
||||
pub roast_date: Option<NaiveDate>,
|
||||
pub amount: f64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -42,6 +44,8 @@ pub struct UpdateBag {
|
|||
pub remaining: Option<f64>,
|
||||
pub closed: Option<bool>,
|
||||
pub finished_at: Option<NaiveDate>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Filter criteria for bag queries.
|
||||
|
|
@ -141,7 +145,7 @@ pub fn bag_timeline_event(
|
|||
entity_type: "bag".to_string(),
|
||||
entity_id: bag.id.into_inner(),
|
||||
action: action.to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: bag.created_at,
|
||||
title: roast.name.clone(),
|
||||
details: vec![
|
||||
TimelineEventDetail {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ impl BrewWithDetails {
|
|||
entity_type: "brew".to_string(),
|
||||
entity_id: self.brew.id.into_inner(),
|
||||
action: "brewed".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: self.brew.created_at,
|
||||
title: self.roast_name.clone(),
|
||||
details,
|
||||
tasting_notes: vec![],
|
||||
|
|
@ -187,6 +187,8 @@ pub struct NewBrew {
|
|||
pub water_volume: i32,
|
||||
pub water_temp: f64,
|
||||
pub quick_notes: Vec<QuickNote>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Filter criteria for brew queries.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl Cafe {
|
|||
entity_type: "cafe".to_string(),
|
||||
entity_id: self.id.into_inner(),
|
||||
action: "added".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: self.created_at,
|
||||
title: self.name.clone(),
|
||||
details: vec![
|
||||
TimelineEventDetail {
|
||||
|
|
@ -53,6 +53,8 @@ pub struct NewCafe {
|
|||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub website: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewCafe {
|
||||
|
|
@ -88,6 +90,8 @@ pub struct UpdateCafe {
|
|||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub website: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl CupWithDetails {
|
|||
entity_type: "cup".to_string(),
|
||||
entity_id: self.cup.id.into_inner(),
|
||||
action: "added".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: self.cup.created_at,
|
||||
title: self.roast_name.clone(),
|
||||
details: vec![
|
||||
TimelineEventDetail {
|
||||
|
|
@ -61,6 +61,8 @@ impl CupWithDetails {
|
|||
pub struct NewCup {
|
||||
pub roast_id: RoastId,
|
||||
pub cafe_id: CafeId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Filter criteria for cup queries.
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ impl Gear {
|
|||
entity_type: "gear".to_string(),
|
||||
entity_id: self.id.into_inner(),
|
||||
action: "added".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: self.created_at,
|
||||
title: format!("{} {}", self.make, self.model),
|
||||
details: vec![
|
||||
TimelineEventDetail {
|
||||
|
|
@ -92,12 +92,16 @@ pub struct NewGear {
|
|||
pub category: GearCategory,
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateGear {
|
||||
pub make: Option<String>,
|
||||
pub model: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ pub struct NewRoaster {
|
|||
pub country: String,
|
||||
pub city: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewRoaster {
|
||||
|
|
@ -77,7 +79,7 @@ impl Roaster {
|
|||
entity_type: "roaster".to_string(),
|
||||
entity_id: self.id.into_inner(),
|
||||
action: "added".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: self.created_at,
|
||||
title: self.name.clone(),
|
||||
details,
|
||||
tasting_notes: vec![],
|
||||
|
|
@ -94,6 +96,8 @@ pub struct UpdateRoaster {
|
|||
pub country: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl UpdateRoaster {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ pub struct NewRoast {
|
|||
pub producer: String,
|
||||
pub tasting_notes: Vec<String>,
|
||||
pub process: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NewRoast {
|
||||
|
|
@ -54,6 +56,8 @@ pub struct UpdateRoast {
|
|||
pub producer: Option<String>,
|
||||
pub tasting_notes: Option<Vec<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)]
|
||||
|
|
@ -120,7 +124,7 @@ pub fn roast_timeline_event(roast: &Roast, roaster: &Roaster) -> NewTimelineEven
|
|||
entity_type: "roast".to_string(),
|
||||
entity_id: roast.id.into_inner(),
|
||||
action: "added".to_string(),
|
||||
occurred_at: Utc::now(),
|
||||
occurred_at: roast.created_at,
|
||||
title: roast.name.clone(),
|
||||
details,
|
||||
tasting_notes: roast.tasting_notes.clone(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
|
||||
use crate::domain::bags::{BagWithRoast, UpdateBag};
|
||||
use crate::domain::ids::{BagId, RoastId};
|
||||
|
|
@ -20,13 +20,17 @@ impl<'a> BagsClient<'a> {
|
|||
roast_id: RoastId,
|
||||
roast_date: Option<NaiveDate>,
|
||||
amount: f64,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
) -> Result<BagWithRoast> {
|
||||
let url = self.inner.endpoint("api/v1/bags")?;
|
||||
let payload = serde_json::json!({
|
||||
let mut payload = serde_json::json!({
|
||||
"roast_id": roast_id,
|
||||
"roast_date": roast_date.map(|d| d.to_string()),
|
||||
"amount": amount,
|
||||
});
|
||||
if let Some(ts) = created_at {
|
||||
payload["created_at"] = serde_json::json!(ts);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
|
|
@ -74,12 +78,14 @@ impl<'a> BagsClient<'a> {
|
|||
remaining: Option<f64>,
|
||||
closed: Option<bool>,
|
||||
finished_at: Option<NaiveDate>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
) -> Result<BagWithRoast> {
|
||||
let url = self.inner.endpoint(&format!("api/v1/bags/{id}"))?;
|
||||
let payload = UpdateBag {
|
||||
remaining,
|
||||
closed,
|
||||
finished_at,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let response = self
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::domain::brews::{BrewWithDetails, QuickNote};
|
||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||
|
|
@ -26,6 +27,7 @@ impl<'a> BrewsClient<'a> {
|
|||
water_volume: i32,
|
||||
water_temp: f64,
|
||||
quick_notes: Vec<QuickNote>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
) -> Result<BrewWithDetails> {
|
||||
let url = self.inner.endpoint("api/v1/brews")?;
|
||||
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();
|
||||
payload["quick_notes"] = serde_json::json!(labels);
|
||||
}
|
||||
if let Some(ts) = created_at {
|
||||
payload["created_at"] = serde_json::json!(ts);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::domain::gear::{Gear, UpdateGear};
|
||||
use crate::domain::ids::GearId;
|
||||
|
|
@ -14,13 +15,22 @@ impl<'a> GearClient<'a> {
|
|||
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 payload = serde_json::json!({
|
||||
let mut payload = serde_json::json!({
|
||||
"category": category,
|
||||
"make": make,
|
||||
"model": model,
|
||||
});
|
||||
if let Some(ts) = created_at {
|
||||
payload["created_at"] = serde_json::json!(ts);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.inner
|
||||
|
|
@ -66,9 +76,14 @@ impl<'a> GearClient<'a> {
|
|||
id: GearId,
|
||||
make: Option<String>,
|
||||
model: Option<String>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
) -> Result<Gear> {
|
||||
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
|
||||
.inner
|
||||
|
|
|
|||
|
|
@ -112,9 +112,10 @@ impl SqlBagRepository {
|
|||
#[async_trait]
|
||||
impl BagRepository for SqlBagRepository {
|
||||
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError> {
|
||||
let created_at = bag.created_at.unwrap_or_else(Utc::now);
|
||||
let query = r"
|
||||
INSERT INTO bags (roast_id, roast_date, amount, remaining)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO bags (roast_id, roast_date, amount, remaining, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
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.amount)
|
||||
.bind(bag.amount) // remaining starts as amount
|
||||
.bind(created_at)
|
||||
.bind(created_at)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.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, "closed", changes.closed);
|
||||
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
|
||||
|
||||
builder.push(" WHERE id = ");
|
||||
|
|
|
|||
|
|
@ -163,9 +163,10 @@ impl BrewRepository for SqlBrewRepository {
|
|||
}
|
||||
|
||||
// Insert the brew
|
||||
let created_at = brew.created_at.unwrap_or_else(Utc::now);
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
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_temp)
|
||||
.bind(Self::encode_quick_notes(&brew.quick_notes))
|
||||
.bind(created_at)
|
||||
.bind(created_at)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.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> {
|
||||
let new_cafe = new_cafe.normalize();
|
||||
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>(
|
||||
"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, "longitude", changes.longitude);
|
||||
push_update_field!(builder, sep, "website", changes.website);
|
||||
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||
let _ = sep;
|
||||
|
||||
builder.push(" WHERE id = ");
|
||||
|
|
|
|||
|
|
@ -107,12 +107,15 @@ impl SqlCupRepository {
|
|||
#[async_trait]
|
||||
impl CupRepository for SqlCupRepository {
|
||||
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>(
|
||||
"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",
|
||||
)
|
||||
.bind(new_cup.roast_id.into_inner())
|
||||
.bind(new_cup.cafe_id.into_inner())
|
||||
.bind(created_at)
|
||||
.bind(created_at)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||
|
|
|
|||
|
|
@ -63,9 +63,10 @@ impl SqlGearRepository {
|
|||
#[async_trait]
|
||||
impl GearRepository for SqlGearRepository {
|
||||
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
|
||||
let created_at = gear.created_at.unwrap_or_else(Utc::now);
|
||||
let query = r"
|
||||
INSERT INTO gear (category, make, model)
|
||||
VALUES (?, ?, ?)
|
||||
INSERT INTO gear (category, make, model, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, category, make, model, created_at, updated_at
|
||||
";
|
||||
|
||||
|
|
@ -73,6 +74,8 @@ impl GearRepository for SqlGearRepository {
|
|||
.bind(gear.category.as_str())
|
||||
.bind(&gear.make)
|
||||
.bind(&gear.model)
|
||||
.bind(created_at)
|
||||
.bind(created_at)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.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, "model", changes.model);
|
||||
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||
let _ = sep; // Suppress unused_assignments warning
|
||||
|
||||
builder.push(" WHERE id = ");
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
|||
async fn insert(&self, new_roaster: NewRoaster) -> Result<Roaster, RepositoryError> {
|
||||
let new_roaster = new_roaster.normalize();
|
||||
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>(
|
||||
"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, "city", changes.city);
|
||||
push_update_field!(builder, sep, "homepage", changes.homepage);
|
||||
push_update_field!(builder, sep, "created_at", changes.created_at);
|
||||
|
||||
if !sep {
|
||||
return Err(RepositoryError::unexpected(
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ impl RoastRepository for SqlRoastRepository {
|
|||
producer,
|
||||
tasting_notes,
|
||||
process,
|
||||
created_at,
|
||||
} = new_roast;
|
||||
|
||||
let origin_value = empty_to_none(origin);
|
||||
|
|
@ -74,7 +75,7 @@ impl RoastRepository for SqlRoastRepository {
|
|||
let producer_value = empty_to_none(producer);
|
||||
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 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, "producer", changes.producer);
|
||||
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
|
||||
if let Some(tasting_notes) = changes.tasting_notes {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::ids::{BagId, RoastId};
|
||||
use crate::infrastructure::client::BrewlogClient;
|
||||
|
|
@ -38,6 +39,9 @@ pub struct AddBagCommand {
|
|||
pub roast_date: Option<String>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
|
|
@ -45,9 +49,18 @@ pub async fn add_bag(client: &BrewlogClient, command: AddBagCommand) -> Result<(
|
|||
.roast_date
|
||||
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
||||
.transpose()?;
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let bag = client
|
||||
.bags()
|
||||
.create(RoastId::new(command.roast_id), roast_date, command.amount)
|
||||
.create(
|
||||
RoastId::new(command.roast_id),
|
||||
roast_date,
|
||||
command.amount,
|
||||
created_at,
|
||||
)
|
||||
.await?;
|
||||
print_json(&bag)
|
||||
}
|
||||
|
|
@ -78,6 +91,9 @@ pub struct UpdateBagCommand {
|
|||
pub closed: Option<bool>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
|
|
@ -85,6 +101,10 @@ pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Re
|
|||
.finished_at
|
||||
.map(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d"))
|
||||
.transpose()?;
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
|
||||
let bag = client
|
||||
.bags()
|
||||
|
|
@ -93,6 +113,7 @@ pub async fn update_bag(client: &BrewlogClient, command: UpdateBagCommand) -> Re
|
|||
command.remaining,
|
||||
command.closed,
|
||||
finished_at,
|
||||
created_at,
|
||||
)
|
||||
.await?;
|
||||
print_json(&bag)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::brews::QuickNote;
|
||||
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)
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
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<()> {
|
||||
|
|
@ -73,6 +78,10 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
|||
.iter()
|
||||
.filter_map(|s| QuickNote::from_str_value(s))
|
||||
.collect();
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
|
||||
let brew = client
|
||||
.brews()
|
||||
|
|
@ -86,6 +95,7 @@ pub async fn add_brew(client: &BrewlogClient, command: AddBrewCommand) -> Result
|
|||
command.water_volume,
|
||||
command.water_temp,
|
||||
quick_notes,
|
||||
created_at,
|
||||
)
|
||||
.await?;
|
||||
print_json(&brew)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::cafes::{NewCafe, UpdateCafe};
|
||||
use crate::domain::ids::CafeId;
|
||||
|
|
@ -45,9 +46,16 @@ pub struct AddCafeCommand {
|
|||
pub longitude: f64,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = NewCafe {
|
||||
name: command.name,
|
||||
city: command.city,
|
||||
|
|
@ -55,6 +63,7 @@ pub async fn add_cafe(client: &BrewlogClient, command: AddCafeCommand) -> Result
|
|||
latitude: command.latitude,
|
||||
longitude: command.longitude,
|
||||
website: command.website,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let cafe = client.cafes().create(&payload).await?;
|
||||
|
|
@ -84,9 +93,16 @@ pub struct UpdateCafeCommand {
|
|||
pub longitude: Option<f64>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = UpdateCafe {
|
||||
name: command.name,
|
||||
city: command.city,
|
||||
|
|
@ -94,6 +110,7 @@ pub async fn update_cafe(client: &BrewlogClient, command: UpdateCafeCommand) ->
|
|||
latitude: command.latitude,
|
||||
longitude: command.longitude,
|
||||
website: command.website,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let cafe = client
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::cups::NewCup;
|
||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||
|
|
@ -34,12 +35,20 @@ pub struct AddCupCommand {
|
|||
pub roast_id: i64,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = NewCup {
|
||||
roast_id: RoastId::new(command.roast_id),
|
||||
cafe_id: CafeId::new(command.cafe_id),
|
||||
created_at,
|
||||
};
|
||||
|
||||
let cup = client.cups().create(&payload).await?;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::ids::GearId;
|
||||
use crate::infrastructure::client::BrewlogClient;
|
||||
|
|
@ -38,12 +39,19 @@ pub struct AddGearCommand {
|
|||
pub make: String,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let gear = client
|
||||
.gear()
|
||||
.create(&command.category, command.make, command.model)
|
||||
.create(&command.category, command.make, command.model, created_at)
|
||||
.await?;
|
||||
print_json(&gear)
|
||||
}
|
||||
|
|
@ -69,12 +77,24 @@ pub struct UpdateGearCommand {
|
|||
pub make: Option<String>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let gear = client
|
||||
.gear()
|
||||
.update(GearId::new(command.id), command.make, command.model)
|
||||
.update(
|
||||
GearId::new(command.id),
|
||||
command.make,
|
||||
command.model,
|
||||
created_at,
|
||||
)
|
||||
.await?;
|
||||
print_json(&gear)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ pub mod tokens;
|
|||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
|
||||
use backup::{BackupCommand, RestoreCommand};
|
||||
use bags::BagCommands;
|
||||
use brews::BrewCommands;
|
||||
|
|
@ -129,6 +131,18 @@ pub struct ServeCommand {
|
|||
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<()>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::ids::RoasterId;
|
||||
use crate::domain::roasters::{NewRoaster, UpdateRoaster};
|
||||
|
|
@ -41,14 +42,22 @@ pub struct AddRoasterCommand {
|
|||
pub city: Option<String>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = NewRoaster {
|
||||
name: command.name,
|
||||
country: command.country,
|
||||
city: command.city,
|
||||
homepage: command.homepage,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let roaster = client.roasters().create(&payload).await?;
|
||||
|
|
@ -74,14 +83,22 @@ pub struct UpdateRoasterCommand {
|
|||
pub city: Option<String>,
|
||||
#[arg(long)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = UpdateRoaster {
|
||||
name: command.name,
|
||||
country: command.country,
|
||||
city: command.city,
|
||||
homepage: command.homepage,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let roaster = client
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use clap::{Args, Subcommand};
|
||||
|
||||
use super::macros::{define_delete_command, define_get_command};
|
||||
use super::parse_created_at;
|
||||
use super::print_json;
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::roasts::{NewRoast, UpdateRoast};
|
||||
|
|
@ -47,9 +48,16 @@ pub struct AddRoastCommand {
|
|||
pub process: String,
|
||||
#[arg(long = "tasting-notes", required = true)]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = NewRoast {
|
||||
roaster_id: RoasterId::new(command.roaster_id),
|
||||
name: command.name,
|
||||
|
|
@ -58,6 +66,7 @@ pub async fn add_roast(client: &BrewlogClient, command: AddRoastCommand) -> Resu
|
|||
producer: command.producer,
|
||||
tasting_notes: command.tasting_notes,
|
||||
process: command.process,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let roast = client.roasts().create(&payload).await?;
|
||||
|
|
@ -98,9 +107,16 @@ pub struct UpdateRoastCommand {
|
|||
pub process: Option<String>,
|
||||
#[arg(long = "tasting-notes")]
|
||||
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<()> {
|
||||
let created_at = command
|
||||
.created_at
|
||||
.map(|s| parse_created_at(&s))
|
||||
.transpose()?;
|
||||
let payload = UpdateRoast {
|
||||
roaster_id: command.roaster_id.map(RoasterId::new),
|
||||
name: command.name,
|
||||
|
|
@ -109,6 +125,7 @@ pub async fn update_roast(client: &BrewlogClient, command: UpdateRoastCommand) -
|
|||
producer: command.producer,
|
||||
tasting_notes: command.tasting_notes,
|
||||
process: command.process,
|
||||
created_at,
|
||||
};
|
||||
|
||||
let roast = client
|
||||
|
|
|
|||
Loading…
Reference in a new issue