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)
153 lines
3.3 KiB
Rust
153 lines
3.3 KiB
Rust
pub mod backup;
|
|
pub mod bags;
|
|
pub mod brews;
|
|
pub mod cafes;
|
|
pub mod cups;
|
|
pub mod gear;
|
|
mod macros;
|
|
pub mod roasters;
|
|
pub mod roasts;
|
|
pub mod tokens;
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
use chrono::{DateTime, NaiveDate, Utc};
|
|
|
|
use backup::{BackupCommand, RestoreCommand};
|
|
use bags::BagCommands;
|
|
use brews::BrewCommands;
|
|
use cafes::CafeCommands;
|
|
use clap::{Args, Parser, Subcommand};
|
|
use cups::CupCommands;
|
|
use gear::GearCommands;
|
|
use roasters::RoasterCommands;
|
|
use roasts::RoastCommands;
|
|
use tokens::TokenCommands;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
|
|
pub struct Cli {
|
|
#[arg(
|
|
long,
|
|
global = true,
|
|
env = "BREWLOG_URL",
|
|
default_value = "http://localhost:3000"
|
|
)]
|
|
pub api_url: String,
|
|
|
|
#[command(subcommand)]
|
|
pub command: Commands,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum Commands {
|
|
/// Run the HTTP server
|
|
Serve(ServeCommand),
|
|
|
|
/// Manage roasters
|
|
Roaster {
|
|
#[command(subcommand)]
|
|
command: RoasterCommands,
|
|
},
|
|
|
|
/// Manage roasts
|
|
Roast {
|
|
#[command(subcommand)]
|
|
command: RoastCommands,
|
|
},
|
|
|
|
/// Manage bags
|
|
Bag {
|
|
#[command(subcommand)]
|
|
command: BagCommands,
|
|
},
|
|
|
|
/// Manage gear
|
|
Gear {
|
|
#[command(subcommand)]
|
|
command: GearCommands,
|
|
},
|
|
|
|
/// Manage brews
|
|
Brew {
|
|
#[command(subcommand)]
|
|
command: BrewCommands,
|
|
},
|
|
|
|
/// Manage cafes
|
|
Cafe {
|
|
#[command(subcommand)]
|
|
command: CafeCommands,
|
|
},
|
|
|
|
/// Manage cups
|
|
Cup {
|
|
#[command(subcommand)]
|
|
command: CupCommands,
|
|
},
|
|
|
|
/// Manage API tokens
|
|
Token {
|
|
#[command(subcommand)]
|
|
command: TokenCommands,
|
|
},
|
|
|
|
/// Back up all coffee data to JSON (stdout)
|
|
Backup(BackupCommand),
|
|
|
|
/// Restore coffee data from a JSON backup file
|
|
Restore(RestoreCommand),
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
pub struct ServeCommand {
|
|
#[arg(
|
|
long,
|
|
env = "BREWLOG_DATABASE_URL",
|
|
default_value = "sqlite://brewlog.db"
|
|
)]
|
|
pub database_url: String,
|
|
|
|
#[arg(long, env = "BREWLOG_BIND_ADDRESS", default_value = "127.0.0.1:3000")]
|
|
pub bind_address: SocketAddr,
|
|
|
|
#[arg(long, env = "BREWLOG_RP_ID")]
|
|
pub rp_id: Option<String>,
|
|
|
|
#[arg(long, env = "BREWLOG_RP_ORIGIN")]
|
|
pub rp_origin: Option<String>,
|
|
|
|
#[arg(long, env = "BREWLOG_OPENROUTER_API_KEY")]
|
|
pub openrouter_api_key: Option<String>,
|
|
|
|
#[arg(
|
|
long,
|
|
env = "BREWLOG_OPENROUTER_MODEL",
|
|
default_value = "openrouter/free"
|
|
)]
|
|
pub openrouter_model: String,
|
|
|
|
#[arg(long, env = "BREWLOG_FOURSQUARE_API_KEY")]
|
|
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,
|
|
{
|
|
let rendered = serde_json::to_string_pretty(value)?;
|
|
println!("{rendered}");
|
|
Ok(())
|
|
}
|