98 lines
2.9 KiB
Rust
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)
|
|
}
|