updated wording
This commit is contained in:
@@ -226,8 +226,15 @@ struct UpdateItemReq {
|
||||
#[validate(length(max = 1000))]
|
||||
note: Option<String>,
|
||||
status: Option<String>,
|
||||
target_price: Option<Decimal>,
|
||||
/// Absent = leave as-is; `null` = clear the target (→ notify on any drop);
|
||||
/// a value = set the target.
|
||||
#[serde(default, deserialize_with = "double_option")]
|
||||
target_price: Option<Option<Decimal>>,
|
||||
position: Option<i32>,
|
||||
/// Manual ISO 4217 currency override (e.g. "EUR"). Wins over the fetched
|
||||
/// currency for items the conversion heuristic mislabels.
|
||||
#[validate(length(equal = 3, message = "currency must be a 3-letter code"))]
|
||||
currency: Option<String>,
|
||||
}
|
||||
|
||||
async fn update_item(
|
||||
@@ -244,15 +251,27 @@ async fn update_item(
|
||||
}
|
||||
}
|
||||
|
||||
// Ownership enforced via the join to lists.user_id.
|
||||
let currency = req
|
||||
.currency
|
||||
.map(|c| c.trim().to_uppercase())
|
||||
.filter(|c| !c.is_empty());
|
||||
|
||||
// target_price: None = leave; Some(v) = set (v may be NULL to clear).
|
||||
let set_target = req.target_price.is_some();
|
||||
let target_val = req.target_price.flatten();
|
||||
|
||||
// Ownership enforced via the join to lists.user_id. A currency override
|
||||
// both latches (currency_override) and takes effect immediately (currency).
|
||||
let item = sqlx::query_as::<_, Item>(&format!(
|
||||
"UPDATE items i SET
|
||||
title = COALESCE($3, i.title),
|
||||
url = COALESCE($4, i.url),
|
||||
note = COALESCE($5, i.note),
|
||||
status = COALESCE($6::item_status, i.status),
|
||||
target_price = COALESCE($7, i.target_price),
|
||||
position = COALESCE($8, i.position)
|
||||
title = COALESCE($3, i.title),
|
||||
url = COALESCE($4, i.url),
|
||||
note = COALESCE($5, i.note),
|
||||
status = COALESCE($6::item_status, i.status),
|
||||
target_price = CASE WHEN $7 THEN $8 ELSE i.target_price END,
|
||||
position = COALESCE($9, i.position),
|
||||
currency_override = COALESCE($10, i.currency_override),
|
||||
currency = COALESCE($10, i.currency)
|
||||
FROM lists l
|
||||
WHERE i.id = $1 AND i.list_id = l.id AND l.user_id = $2
|
||||
RETURNING {ITEM_COLS_I}"
|
||||
@@ -263,8 +282,10 @@ async fn update_item(
|
||||
.bind(opt_trim(req.url))
|
||||
.bind(opt_trim(req.note))
|
||||
.bind(req.status)
|
||||
.bind(req.target_price)
|
||||
.bind(set_target)
|
||||
.bind(target_val)
|
||||
.bind(req.position)
|
||||
.bind(currency)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound)?;
|
||||
@@ -354,3 +375,14 @@ async fn item_history(
|
||||
fn opt_trim(s: Option<String>) -> Option<String> {
|
||||
s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Deserialize a present-but-nullable field into `Option<Option<T>>`, so an
|
||||
/// explicit JSON `null` (`Some(None)`) is distinguishable from an absent field
|
||||
/// (`None`, handled by `#[serde(default)]`). Lets a PATCH clear a value.
|
||||
fn double_option<'de, T, D>(de: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(Some(Option::<T>::deserialize(de)?))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user