refactor(gear): remove notes field from Gear entity

The notes field was not providing enough value to justify its presence.
Simplified the Gear entity by removing notes from:
- Domain structs (Gear, NewGear, UpdateGear)
- SQL repository queries and GearRecord
- HTTP client methods
- CLI commands (--notes flag)
- Web views and templates
- All related tests

Added migration 0008_remove_gear_notes.sql to drop the column.
This commit is contained in:
Jon Seager 2026-02-02 16:16:54 +00:00
parent 708d89d452
commit 6bbbafbef4
No known key found for this signature in database
13 changed files with 17 additions and 106 deletions

View file

@ -0,0 +1 @@
ALTER TABLE gear DROP COLUMN notes;

View file

@ -202,7 +202,6 @@ pub(crate) struct NewGearSubmission {
category: String,
make: String,
model: String,
notes: Option<String>,
}
impl NewGearSubmission {
@ -222,7 +221,6 @@ impl NewGearSubmission {
category,
make: self.make,
model: self.model,
notes: self.notes.filter(|s| !s.trim().is_empty()),
})
}
}

View file

@ -47,7 +47,6 @@ pub struct Gear {
pub category: GearCategory,
pub make: String,
pub model: String,
pub notes: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@ -57,14 +56,12 @@ pub struct NewGear {
pub category: GearCategory,
pub make: String,
pub model: String,
pub notes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateGear {
pub make: Option<String>,
pub model: Option<String>,
pub notes: Option<String>,
}
#[derive(Debug, Default, Clone)]

View file

@ -14,19 +14,12 @@ impl<'a> GearClient<'a> {
Self { inner }
}
pub async fn create(
&self,
category: &str,
make: String,
model: String,
notes: Option<String>,
) -> Result<Gear> {
pub async fn create(&self, category: &str, make: String, model: String) -> Result<Gear> {
let url = self.inner.endpoint("api/v1/gear")?;
let payload = serde_json::json!({
"category": category,
"make": make,
"model": model,
"notes": notes,
});
let response = self
@ -73,10 +66,9 @@ impl<'a> GearClient<'a> {
id: GearId,
make: Option<String>,
model: Option<String>,
notes: Option<String>,
) -> Result<Gear> {
let url = self.inner.endpoint(&format!("api/v1/gear/{id}"))?;
let payload = UpdateGear { make, model, notes };
let payload = UpdateGear { make, model };
let response = self
.inner

View file

@ -46,7 +46,6 @@ impl SqlGearRepository {
category,
make: record.make,
model: record.model,
notes: record.notes,
created_at: record.created_at,
updated_at: record.updated_at,
})
@ -64,16 +63,15 @@ impl SqlGearRepository {
impl GearRepository for SqlGearRepository {
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError> {
let query = r#"
INSERT INTO gear (category, make, model, notes)
VALUES (?, ?, ?, ?)
RETURNING id, category, make, model, notes, created_at, updated_at
INSERT INTO gear (category, make, model)
VALUES (?, ?, ?)
RETURNING id, category, make, model, created_at, updated_at
"#;
let record = query_as::<_, GearRecord>(query)
.bind(gear.category.as_str())
.bind(&gear.make)
.bind(&gear.model)
.bind(&gear.notes)
.fetch_one(&self.pool)
.await
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
@ -83,7 +81,7 @@ impl GearRepository for SqlGearRepository {
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
let query = r#"
SELECT id, category, make, model, notes, created_at, updated_at
SELECT id, category, make, model, created_at, updated_at
FROM gear
WHERE id = ?
"#;
@ -108,11 +106,12 @@ impl GearRepository for SqlGearRepository {
let base_query = match &where_clause {
Some(w) => format!(
"SELECT id, category, make, model, notes, created_at, updated_at FROM gear WHERE {}",
"SELECT id, category, make, model, created_at, updated_at FROM gear WHERE {}",
w
),
None => "SELECT id, category, make, model, notes, created_at, updated_at FROM gear"
.to_string(),
None => {
"SELECT id, category, make, model, created_at, updated_at FROM gear".to_string()
}
};
let count_query = match &where_clause {
@ -137,12 +136,11 @@ 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, "notes", changes.notes);
let _ = sep; // Suppress unused_assignments warning
builder.push(" WHERE id = ");
builder.push_bind(id.into_inner());
builder.push(" RETURNING id, category, make, model, notes, created_at, updated_at");
builder.push(" RETURNING id, category, make, model, created_at, updated_at");
let record = builder
.build_query_as::<GearRecord>()
@ -177,7 +175,6 @@ struct GearRecord {
category: String,
make: String,
model: String,
notes: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}

View file

@ -14,19 +14,12 @@ pub struct AddGearCommand {
pub make: String,
#[arg(long)]
pub model: String,
#[arg(long)]
pub notes: Option<String>,
}
pub async fn add_gear(client: &BrewlogClient, command: AddGearCommand) -> Result<()> {
let gear = client
.gear()
.create(
&command.category,
command.make,
command.model,
command.notes,
)
.create(&command.category, command.make, command.model)
.await?;
print_json(&gear)
}
@ -52,19 +45,12 @@ pub struct UpdateGearCommand {
pub make: Option<String>,
#[arg(long)]
pub model: Option<String>,
#[arg(long)]
pub notes: Option<String>,
}
pub async fn update_gear(client: &BrewlogClient, command: UpdateGearCommand) -> Result<()> {
let gear = client
.gear()
.update(
GearId::new(command.id),
command.make,
command.model,
command.notes,
)
.update(GearId::new(command.id), command.make, command.model)
.await?;
print_json(&gear)
}

View file

@ -565,7 +565,6 @@ pub struct GearView {
pub make: String,
pub model: String,
pub full_name: String,
pub notes: String,
pub created_at: String,
}
@ -578,7 +577,6 @@ impl GearView {
make: gear.make.clone(),
model: gear.model.clone(),
full_name: format!("{} {}", gear.make, gear.model),
notes: gear.notes.unwrap_or_else(|| "No notes.".to_string()),
created_at: gear.created_at.format("%Y-%m-%d").to_string(),
}
}

View file

@ -69,15 +69,6 @@
placeholder="Encore"
/>
</label>
<label class="flex flex-col gap-1 text-sm sm:col-span-2">
<span class="text-stone-700">Notes</span>
<textarea
name="notes"
rows="3"
class="input-field"
placeholder="Additional notes about this equipment..."
></textarea>
</label>
</div>
<div class="flex justify-end gap-3">

View file

@ -19,7 +19,6 @@
{% call table::sortable_header("Category", "category", navigator, "#gear-list") %}
{% call table::sortable_header("Make", "make", navigator, "#gear-list") %}
{% call table::sortable_header("Model", "model", navigator, "#gear-list") %}
<th scope="col" class="px-4 py-3">Notes</th>
{% call table::sortable_header("Added", "created-at", navigator, "#gear-list") %}
{% if is_authenticated %}
<th scope="col" class="relative px-4 py-3">
@ -42,9 +41,6 @@
<td class="px-4 py-3 whitespace-nowrap">
{{ item.model }}
</td>
<td class="px-4 py-3">
<div class="max-w-xs truncate">{{ item.notes }}</div>
</td>
<td class="px-4 py-3 whitespace-nowrap">
{{ item.created_at }}
</td>

View file

@ -50,34 +50,6 @@ fn test_add_gear_with_authentication() {
assert!(gear["id"].is_i64());
}
#[test]
fn test_add_gear_with_notes() {
let token = create_token("test-add-gear-notes");
let output = run_brewlog(
&[
"add-gear",
"--category",
"brewer",
"--make",
"Hario",
"--model",
"V60",
"--notes",
"Size 02 plastic",
],
&[("BREWLOG_TOKEN", &token)],
);
assert!(output.status.success());
let gear: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(gear["make"], "Hario");
assert_eq!(gear["model"], "V60");
assert_eq!(gear["category"], "brewer");
assert_eq!(gear["notes"], "Size 02 plastic");
}
#[test]
fn test_list_gear_works_without_authentication() {
let _ = server_info();
@ -235,15 +207,7 @@ fn test_update_gear_with_authentication() {
// Update gear
let output = run_brewlog(
&[
"update-gear",
"--id",
&gear_id,
"--model",
"Mini II",
"--notes",
"Upgraded version",
],
&["update-gear", "--id", &gear_id, "--model", "Mini II"],
&[("BREWLOG_TOKEN", &token)],
);
@ -251,7 +215,6 @@ fn test_update_gear_with_authentication() {
let updated_gear: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(updated_gear["make"], "Porlex");
assert_eq!(updated_gear["model"], "Mini II");
assert_eq!(updated_gear["notes"], "Upgraded version");
}
#[test]

View file

@ -655,7 +655,6 @@ async fn gear_update_with_datastar_header_returns_fragment() {
let update = brewlog::domain::gear::UpdateGear {
make: Some("Updated Make".to_string()),
model: None,
notes: Some("Test notes".to_string()),
};
let response = client
@ -700,7 +699,6 @@ async fn gear_update_without_datastar_header_returns_json() {
let update = brewlog::domain::gear::UpdateGear {
make: Some("JSON Updated".to_string()),
model: None,
notes: None,
};
let response = client

View file

@ -10,8 +10,7 @@ async fn creating_gear_returns_201_for_valid_data() {
let new_gear = serde_json::json!({
"category": "grinder",
"make": "Baratza",
"model": "Encore",
"notes": "Daily driver"
"model": "Encore"
});
// Act
@ -29,7 +28,6 @@ async fn creating_gear_returns_201_for_valid_data() {
let gear: Gear = response.json().await.expect("Failed to parse response");
assert_eq!(gear.make, "Baratza");
assert_eq!(gear.model, "Encore");
assert_eq!(gear.notes, Some("Daily driver".to_string()));
}
#[tokio::test]
@ -284,7 +282,6 @@ async fn updating_gear_returns_updated_data() {
let update = UpdateGear {
make: Some("Comandante".to_string()),
model: Some("C40".to_string()),
notes: Some("Upgraded grinder".to_string()),
};
let response = client
@ -302,7 +299,6 @@ async fn updating_gear_returns_updated_data() {
assert_eq!(updated_gear.id, created_gear.id);
assert_eq!(updated_gear.make, "Comandante");
assert_eq!(updated_gear.model, "C40");
assert_eq!(updated_gear.notes, Some("Upgraded grinder".to_string()));
}
#[tokio::test]
@ -314,7 +310,6 @@ async fn updating_gear_without_auth_returns_401() {
let update = UpdateGear {
make: Some("Updated".to_string()),
model: None,
notes: None,
};
// Act

View file

@ -421,8 +421,7 @@ async fn creating_gear_surfaces_on_the_timeline() {
let gear_submission = serde_json::json!({
"category": "grinder",
"make": "Baratza",
"model": "Encore",
"notes": "Timeline test grinder"
"model": "Encore"
});
let response = client