feat(reactions): per-user like/dislike on mangas
Add a private per-user reaction resource: a manga_reactions table (one row per user+manga, like/dislike, cascades) and endpoints PUT/DELETE /mangas/:id/reaction plus GET /me/reactions/:manga_id, wired on the existing per-user seam (mirrors bookmarks/read_progress). Unknown manga → 404 via the FK-violation mapping; an invalid reaction value → 422. Reactions are never exposed publicly — this is a taste signal for the upcoming content-based recommendations. Integration tests cover set/toggle/clear/read, per-user isolation, and the 404/422/401 paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ pub mod page;
|
||||
pub mod page_analysis;
|
||||
pub mod page_tag;
|
||||
pub mod patch;
|
||||
pub mod reaction;
|
||||
pub mod read_progress;
|
||||
pub mod session;
|
||||
pub mod storage_stats;
|
||||
|
||||
40
backend/src/domain/reaction.rs
Normal file
40
backend/src/domain/reaction.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// A user's private taste signal on a manga. Stored as text in
|
||||
/// `manga_reactions.reaction` (CHECK-constrained), so we map to/from a
|
||||
/// `&str` at the repo layer rather than deriving a Postgres enum type.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Reaction {
|
||||
Like,
|
||||
Dislike,
|
||||
}
|
||||
|
||||
impl Reaction {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Reaction::Like => "like",
|
||||
Reaction::Dislike => "dislike",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a stored/inbound value. Returns `None` for anything outside the
|
||||
/// closed vocabulary (the DB CHECK guarantees stored rows are valid; this
|
||||
/// also guards the inbound API body).
|
||||
pub fn parse(s: &str) -> Option<Reaction> {
|
||||
match s {
|
||||
"like" => Some(Reaction::Like),
|
||||
"dislike" => Some(Reaction::Dislike),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response shape for `GET /me/reactions/:manga_id` — the current user's
|
||||
/// reaction on one manga, or `null` when they haven't reacted.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MangaReaction {
|
||||
pub manga_id: Uuid,
|
||||
pub reaction: Option<Reaction>,
|
||||
}
|
||||
Reference in New Issue
Block a user