Files
Mangalord/backend/src/repo/page.rs
fabi d51ab2a049
Some checks failed
deploy / test-backend (push) Failing after 19m59s
deploy / test-frontend (push) Successful in 9m54s
deploy / build-and-push (push) Has been skipped
deploy / deploy (push) Has been skipped
feat(admin): observability — job history, live now-analyzing, durations & metrics (0.84.0) (#6)
2026-06-16 12:21:13 +00:00

98 lines
2.9 KiB
Rust

//! Per-page persistence. Mirrors the rows that `pages` holds.
use sqlx::{PgExecutor, PgPool};
use uuid::Uuid;
use crate::domain::Page;
use crate::error::AppResult;
pub async fn create<'e, E: PgExecutor<'e>>(
executor: E,
chapter_id: Uuid,
page_number: i32,
storage_key: &str,
content_type: &str,
size_bytes: i64,
) -> AppResult<Page> {
let row = sqlx::query_as::<_, Page>(
r#"
INSERT INTO pages (chapter_id, page_number, storage_key, content_type, size_bytes)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, chapter_id, page_number, storage_key, content_type
"#,
)
.bind(chapter_id)
.bind(page_number)
.bind(storage_key)
.bind(content_type)
.bind(size_bytes)
.fetch_one(executor)
.await?;
Ok(row)
}
/// Fetch a single page by id. `None` when it doesn't exist (e.g. deleted
/// between an analysis job's enqueue and its dispatch).
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> AppResult<Option<Page>> {
let row = sqlx::query_as::<_, Page>(
"SELECT id, chapter_id, page_number, storage_key, content_type \
FROM pages WHERE id = $1",
)
.bind(id)
.fetch_optional(pool)
.await?;
Ok(row)
}
/// Resolve a page's breadcrumb `(manga_id, chapter_id, page_number)` for
/// live event payloads. `None` if the page doesn't exist.
pub async fn locate(
pool: &PgPool,
page_id: Uuid,
) -> AppResult<Option<(Uuid, Uuid, i32)>> {
let row: Option<(Uuid, Uuid, i32)> = sqlx::query_as(
"SELECT c.manga_id, p.chapter_id, p.page_number \
FROM pages p JOIN chapters c ON c.id = p.chapter_id WHERE p.id = $1",
)
.bind(page_id)
.fetch_optional(pool)
.await?;
Ok(row)
}
/// Like [`locate`] but also resolves the manga title and chapter number, so
/// live analysis events can carry human labels for the "now analyzing"
/// banner without the dashboard having to look them up. Returns
/// `(manga_id, manga_title, chapter_id, chapter_number, page_number)`.
pub async fn locate_labeled(
pool: &PgPool,
page_id: Uuid,
) -> AppResult<Option<(Uuid, String, Uuid, i32, i32)>> {
let row: Option<(Uuid, String, Uuid, i32, i32)> = sqlx::query_as(
"SELECT c.manga_id, m.title, p.chapter_id, c.number, p.page_number \
FROM pages p \
JOIN chapters c ON c.id = p.chapter_id \
JOIN mangas m ON m.id = c.manga_id \
WHERE p.id = $1",
)
.bind(page_id)
.fetch_optional(pool)
.await?;
Ok(row)
}
pub async fn list_for_chapter(pool: &PgPool, chapter_id: Uuid) -> AppResult<Vec<Page>> {
let rows = sqlx::query_as::<_, Page>(
r#"
SELECT id, chapter_id, page_number, storage_key, content_type
FROM pages
WHERE chapter_id = $1
ORDER BY page_number ASC
"#,
)
.bind(chapter_id)
.fetch_all(pool)
.await?;
Ok(rows)
}