//! Releases and their assets (hub spec ยง9). use super::{now_ms, Pool}; use sqlx::FromRow; #[derive(Clone, Debug, FromRow)] pub struct Release { pub id: i64, pub repo_id: i64, pub tag: String, pub title: String, pub notes: String, pub prerelease: bool, pub author_id: Option, pub author: String, pub created_ms: i64, } #[derive(Clone, Debug, FromRow)] pub struct Asset { pub id: i64, pub release_id: i64, pub name: String, pub size: i64, pub hash: String, pub content_type: String, pub downloads: i64, pub created_ms: i64, } pub async fn list(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 ORDER BY created_ms DESC, id DESC").bind(repo_id).fetch_all(pool).await } pub async fn get(pool: &Pool, repo_id: i64, tag: &str) -> sqlx::Result> { sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 AND tag = $2").bind(repo_id).bind(tag).fetch_optional(pool).await } /// The newest release that is not a pre-release: the "latest" marker. pub async fn latest(pool: &Pool, repo_id: i64) -> sqlx::Result> { sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 AND prerelease = FALSE ORDER BY created_ms DESC, id DESC LIMIT 1") .bind(repo_id) .fetch_optional(pool) .await } pub async fn create( pool: &Pool, repo_id: i64, tag: &str, title: &str, notes: &str, prerelease: bool, author: Option<(i64, &str)>, ) -> sqlx::Result { sqlx::query_as("INSERT INTO releases (repo_id, tag, title, notes, prerelease, author_id, author, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *") .bind(repo_id) .bind(tag) .bind(title) .bind(notes) .bind(prerelease) .bind(author.map(|a| a.0)) .bind(author.map(|a| a.1).unwrap_or("")) .bind(now_ms()) .fetch_one(pool) .await } pub async fn edit(pool: &Pool, id: i64, title: &str, notes: &str, prerelease: bool) -> sqlx::Result<()> { sqlx::query("UPDATE releases SET title = $1, notes = $2, prerelease = $3 WHERE id = $4") .bind(title) .bind(notes) .bind(prerelease) .bind(id) .execute(pool) .await?; Ok(()) } pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> { sqlx::query("DELETE FROM releases WHERE id = $1").bind(id).execute(pool).await?; Ok(()) } pub async fn assets(pool: &Pool, release_id: i64) -> sqlx::Result> { sqlx::query_as("SELECT * FROM release_assets WHERE release_id = $1 ORDER BY name").bind(release_id).fetch_all(pool).await } pub async fn asset(pool: &Pool, release_id: i64, name: &str) -> sqlx::Result> { sqlx::query_as("SELECT * FROM release_assets WHERE release_id = $1 AND name = $2") .bind(release_id) .bind(name) .fetch_optional(pool) .await } pub async fn add_asset(pool: &Pool, release_id: i64, name: &str, size: i64, hash: &str, content_type: &str) -> sqlx::Result { sqlx::query_as( "INSERT INTO release_assets (release_id, name, size, hash, content_type, created_ms) VALUES ($1, $2, $3, $4, $5, $6) \ ON CONFLICT (release_id, name) DO UPDATE SET size = excluded.size, hash = excluded.hash, content_type = excluded.content_type, created_ms = excluded.created_ms RETURNING *", ) .bind(release_id) .bind(name) .bind(size) .bind(hash) .bind(content_type) .bind(now_ms()) .fetch_one(pool) .await } pub async fn delete_asset(pool: &Pool, id: i64) -> sqlx::Result<()> { sqlx::query("DELETE FROM release_assets WHERE id = $1").bind(id).execute(pool).await?; Ok(()) } pub async fn count_download(pool: &Pool, id: i64) -> sqlx::Result<()> { sqlx::query("UPDATE release_assets SET downloads = downloads + 1 WHERE id = $1").bind(id).execute(pool).await?; Ok(()) } /// Whether any asset of any release still names this content. pub async fn hash_in_use(pool: &Pool, hash: &str) -> sqlx::Result { let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM release_assets WHERE hash = $1").bind(hash).fetch_one(pool).await?; Ok(n > 0) }