bugfix: bookmark chapter links use chapter number, not UUID
The reader route is keyed on chapter number (URL `/manga/{id}/chapter/{n}`,
loaded via `Number(params.n)`), but the bookmarks list was building
hrefs from `chapter_id` (a UUID). Following any chapter bookmark
produced a NaN load on the reader page.
Fix at the API layer so every consumer of /me/bookmarks gets the
information without a follow-up round-trip per bookmark.
- domain::BookmarkSummary: new type, `Bookmark` plus
`chapter_number: Option<i32>`. Populated by a LEFT JOIN on chapters
so manga-level bookmarks come back with `chapter_number = null` and
chapter-level ones get the value. `Bookmark` itself stays minimal
for POST / DELETE responses.
- repo::bookmark::list_for_user returns Vec<BookmarkSummary>.
- api::bookmarks::list_me returns PagedResponse<BookmarkSummary>.
- Frontend `Bookmark` type carries an optional `chapter_number`.
- /bookmarks page builds `/manga/{manga_id}/chapter/{chapter_number}`
for chapter bookmarks, falling back to the manga overview if the
chapter has been deleted out from under the bookmark (chapter_id is
ON DELETE SET NULL, so this is a real edge case).
New test asserts both branches of the JOIN: a chapter-level bookmark
comes back with the right chapter_number and page, a manga-level one
has a null chapter_number.
Lockstep version bump to 0.9.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,81 @@ async fn list_me_requires_authentication(pool: PgPool) {
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn list_me_enriches_chapter_bookmarks_with_chapter_number(pool: PgPool) {
|
||||
let h = common::harness(pool.clone());
|
||||
let (_, cookie) = common::register_user(&h.app).await;
|
||||
let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
|
||||
// Seed a chapter directly so we know its number without uploading pages.
|
||||
mangalord::repo::chapter::create(&pool, manga_id, 7, Some("The Brand"))
|
||||
.await
|
||||
.unwrap();
|
||||
// Look up its id so we can bookmark it.
|
||||
let chapter_id: Uuid = sqlx::query_scalar(
|
||||
"SELECT id FROM chapters WHERE manga_id = $1 AND number = $2",
|
||||
)
|
||||
.bind(manga_id)
|
||||
.bind(7_i32)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Bookmark the chapter at page 4.
|
||||
let resp = h
|
||||
.app
|
||||
.clone()
|
||||
.oneshot(common::post_json_with_cookie(
|
||||
"/api/v1/bookmarks",
|
||||
json!({
|
||||
"manga_id": manga_id.to_string(),
|
||||
"chapter_id": chapter_id.to_string(),
|
||||
"page": 4
|
||||
}),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CREATED);
|
||||
|
||||
// Also bookmark the manga at the manga level — chapter_number must
|
||||
// come back null for it, populated for the chapter one.
|
||||
let _ = h
|
||||
.app
|
||||
.clone()
|
||||
.oneshot(common::post_json_with_cookie(
|
||||
"/api/v1/bookmarks",
|
||||
json!({ "manga_id": manga_id.to_string() }),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp = h
|
||||
.app
|
||||
.oneshot(common::get_with_cookie("/api/v1/me/bookmarks", &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
let body = common::body_json(resp).await;
|
||||
let items = body["items"].as_array().unwrap();
|
||||
assert_eq!(items.len(), 2);
|
||||
|
||||
let chapter_bookmark = items
|
||||
.iter()
|
||||
.find(|b| !b["chapter_id"].is_null())
|
||||
.expect("chapter-level bookmark present");
|
||||
assert_eq!(chapter_bookmark["chapter_number"], 7);
|
||||
assert_eq!(chapter_bookmark["page"], 4);
|
||||
|
||||
let manga_bookmark = items
|
||||
.iter()
|
||||
.find(|b| b["chapter_id"].is_null())
|
||||
.expect("manga-level bookmark present");
|
||||
assert!(
|
||||
manga_bookmark["chapter_number"].is_null(),
|
||||
"manga-level bookmark must have a null chapter_number"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn list_me_returns_paged_envelope(pool: PgPool) {
|
||||
let h = common::harness(pool);
|
||||
|
||||
Reference in New Issue
Block a user