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:
@@ -3,7 +3,7 @@
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::Bookmark;
|
||||
use crate::domain::{Bookmark, BookmarkSummary};
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
pub async fn create(
|
||||
@@ -36,18 +36,31 @@ pub async fn create(
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the user's bookmarks enriched with each chapter's number
|
||||
/// (LEFT JOINed; `chapter_number` is `null` for manga-level bookmarks
|
||||
/// or for chapter bookmarks whose chapter has since been deleted —
|
||||
/// `bookmarks.chapter_id` is `ON DELETE SET NULL`). The frontend uses
|
||||
/// the number to build reader URLs, which are keyed on number, not id.
|
||||
pub async fn list_for_user(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> AppResult<Vec<Bookmark>> {
|
||||
let rows = sqlx::query_as::<_, Bookmark>(
|
||||
) -> AppResult<Vec<BookmarkSummary>> {
|
||||
let rows = sqlx::query_as::<_, BookmarkSummary>(
|
||||
r#"
|
||||
SELECT id, user_id, manga_id, chapter_id, page, created_at
|
||||
FROM bookmarks
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
SELECT
|
||||
b.id,
|
||||
b.user_id,
|
||||
b.manga_id,
|
||||
b.chapter_id,
|
||||
c.number AS chapter_number,
|
||||
b.page,
|
||||
b.created_at
|
||||
FROM bookmarks b
|
||||
LEFT JOIN chapters c ON c.id = b.chapter_id
|
||||
WHERE b.user_id = $1
|
||||
ORDER BY b.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user