gitdataai/lib/service/git/release.rs

448 lines
14 KiB
Rust

use chrono::Utc;
use db::sqlx;
use model::repos::{RepoReleaseAssetModel, RepoReleaseModel};
use session::Session;
use uuid::Uuid;
use crate::AppService;
use crate::error::AppError;
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ReleaseResponse {
pub id: Uuid,
pub tag_name: String,
pub target_commit_sha: String,
pub name: String,
pub body: Option<String>,
pub draft: bool,
pub prerelease: bool,
pub author: Uuid,
pub assets: Vec<ReleaseAssetResponse>,
pub published_at: Option<chrono::DateTime<Utc>>,
pub created_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ReleaseAssetResponse {
pub id: Uuid,
pub name: String,
pub content_type: Option<String>,
pub size: i64,
pub download_count: i64,
pub created_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateRelease {
pub tag_name: String,
pub target_commit_sha: Option<String>,
pub name: String,
pub body: Option<String>,
#[serde(default)]
pub draft: bool,
#[serde(default)]
pub prerelease: bool,
}
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateRelease {
pub tag_name: Option<String>,
pub name: Option<String>,
pub body: Option<Option<String>>,
pub draft: Option<bool>,
pub prerelease: Option<bool>,
}
impl AppService {
pub async fn git_release_list(
&self,
repo_id: Uuid,
) -> Result<Vec<ReleaseResponse>, AppError> {
let releases = sqlx::query_as::<_, RepoReleaseModel>(
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at \
FROM repo_release WHERE repo = $1 ORDER BY created_at DESC LIMIT 50",
)
.bind(repo_id)
.fetch_all(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
let mut result = Vec::with_capacity(releases.len());
for r in releases {
let assets = self.git_release_assets(r.id).await?;
result.push(release_to_response(r, assets));
}
Ok(result)
}
pub async fn git_release_get(
&self,
repo_id: Uuid,
release_id: Uuid,
) -> Result<ReleaseResponse, AppError> {
let r = sqlx::query_as::<_, RepoReleaseModel>(
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at \
FROM repo_release WHERE id = $1 AND repo = $2",
)
.bind(release_id)
.bind(repo_id)
.fetch_optional(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?
.ok_or_else(|| AppError::NotFound("release not found".to_string()))?;
let assets = self.git_release_assets(r.id).await?;
Ok(release_to_response(r, assets))
}
pub async fn git_release_get_by_tag(
&self,
repo_id: Uuid,
tag: &str,
) -> Result<ReleaseResponse, AppError> {
let r = sqlx::query_as::<_, RepoReleaseModel>(
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at \
FROM repo_release WHERE repo = $1 AND tag_name = $2",
)
.bind(repo_id)
.bind(tag)
.fetch_optional(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?
.ok_or_else(|| AppError::NotFound("release not found".to_string()))?;
let assets = self.git_release_assets(r.id).await?;
Ok(release_to_response(r, assets))
}
pub async fn git_release_create(
&self,
_session: &Session,
repo_id: Uuid,
user_id: Uuid,
params: CreateRelease,
) -> Result<ReleaseResponse, AppError> {
let id = Uuid::now_v7();
let now = Utc::now();
let published_at = if params.draft { None } else { Some(now) };
let target = if let Some(ref sha) = params.target_commit_sha {
if !sha.trim().is_empty() {
sha.clone()
} else {
self.default_branch_sha(repo_id).await?
}
} else {
self.default_branch_sha(repo_id).await?
};
let r = sqlx::query_as::<_, RepoReleaseModel>(
"INSERT INTO repo_release (id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at) \
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$11) \
RETURNING id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at",
)
.bind(id)
.bind(repo_id)
.bind(&params.tag_name)
.bind(&target)
.bind(&params.name)
.bind(&params.body)
.bind(params.draft)
.bind(params.prerelease)
.bind(user_id)
.bind(published_at)
.bind(now)
.fetch_one(self.db.writer())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
Ok(release_to_response(r, Vec::new()))
}
pub async fn git_release_update(
&self,
repo_id: Uuid,
release_id: Uuid,
params: UpdateRelease,
) -> Result<ReleaseResponse, AppError> {
let existing = self.git_release_get(repo_id, release_id).await?;
let now = Utc::now();
let tag_name = params.tag_name.unwrap_or(existing.tag_name);
let name = params.name.unwrap_or(existing.name);
let body = params.body.unwrap_or(existing.body);
let draft = params.draft.unwrap_or(existing.draft);
let prerelease = params.prerelease.unwrap_or(existing.prerelease);
let published_at = if draft {
None
} else {
existing.published_at.or(Some(now))
};
let r = sqlx::query_as::<_, RepoReleaseModel>(
"UPDATE repo_release SET tag_name=$1, name=$2, body=$3, draft=$4, \
prerelease=$5, published_at=$6, updated_at=$7 \
WHERE id=$8 AND repo=$9 \
RETURNING id, repo, tag_name, target_commit_sha, name, body, \
draft, prerelease, author, published_at, created_at, updated_at",
)
.bind(&tag_name)
.bind(&name)
.bind(&body)
.bind(draft)
.bind(prerelease)
.bind(published_at)
.bind(now)
.bind(release_id)
.bind(repo_id)
.fetch_one(self.db.writer())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
let assets = self.git_release_assets(r.id).await?;
Ok(release_to_response(r, assets))
}
pub async fn git_release_delete(
&self,
repo_id: Uuid,
release_id: Uuid,
) -> Result<(), AppError> {
let rows =
sqlx::query("DELETE FROM repo_release WHERE id = $1 AND repo = $2")
.bind(release_id)
.bind(repo_id)
.execute(self.db.writer())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
if rows.rows_affected() == 0 {
return Err(AppError::NotFound("release not found".to_string()));
}
Ok(())
}
pub async fn git_release_delete_by_tag(
&self,
repo_id: Uuid,
tag: &str,
) -> Result<(), AppError> {
let release_id: Option<Uuid> = sqlx::query_scalar(
"SELECT id FROM repo_release WHERE repo = $1 AND tag_name = $2",
)
.bind(repo_id)
.bind(tag)
.fetch_optional(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
let release_id = release_id.ok_or_else(|| {
AppError::NotFound("release not found".to_string())
})?;
self.git_release_delete(repo_id, release_id).await
}
async fn git_release_assets(
&self,
release_id: Uuid,
) -> Result<Vec<ReleaseAssetResponse>, AppError> {
let assets = sqlx::query_as::<_, RepoReleaseAssetModel>(
"SELECT id, release_id, name, content_type, size, download_count, \
storage_path, uploader, created_at \
FROM repo_release_asset WHERE release_id = $1 ORDER BY created_at",
)
.bind(release_id)
.fetch_all(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
Ok(assets
.into_iter()
.map(|a| ReleaseAssetResponse {
id: a.id,
name: a.name,
content_type: a.content_type,
size: a.size,
download_count: a.download_count,
created_at: a.created_at,
})
.collect())
}
pub async fn git_release_asset_create(
&self,
repo_id: Uuid,
release_id: Uuid,
user_id: Uuid,
name: String,
content_type: Option<String>,
size: i64,
storage_path: String,
) -> Result<ReleaseAssetResponse, AppError> {
let _ = self.git_release_get(repo_id, release_id).await?;
let id = Uuid::now_v7();
let now = Utc::now();
let a = sqlx::query_as::<_, RepoReleaseAssetModel>(
"INSERT INTO repo_release_asset (id, release_id, name, content_type, size, \
download_count, storage_path, uploader, created_at) \
VALUES ($1,$2,$3,$4,$5,0,$6,$7,$8) RETURNING *",
)
.bind(id)
.bind(release_id)
.bind(&name)
.bind(&content_type)
.bind(size)
.bind(&storage_path)
.bind(user_id)
.bind(now)
.fetch_one(self.db.writer())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
Ok(ReleaseAssetResponse {
id: a.id,
name: a.name,
content_type: a.content_type,
size: a.size,
download_count: a.download_count,
created_at: a.created_at,
})
}
pub async fn git_release_asset_delete(
&self,
repo_id: Uuid,
release_id: Uuid,
asset_id: Uuid,
) -> Result<(), AppError> {
let _ = self.git_release_get(repo_id, release_id).await?;
sqlx::query(
"DELETE FROM repo_release_asset WHERE id = $1 AND release_id = $2",
)
.bind(asset_id)
.bind(release_id)
.execute(self.db.writer())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
Ok(())
}
}
impl AppService {
pub async fn git_release_list_by_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
) -> Result<Vec<ReleaseResponse>, AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_list(repo.id).await
}
pub async fn git_release_get_by_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
id: Uuid,
) -> Result<ReleaseResponse, AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_get(repo.id, id).await
}
pub async fn git_release_get_by_tag_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
tag: &str,
) -> Result<ReleaseResponse, AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_get_by_tag(repo.id, tag).await
}
pub async fn git_release_create_by_name(
&self,
ctx: &Session,
user_id: Uuid,
wk: &str,
repo: &str,
params: CreateRelease,
) -> Result<ReleaseResponse, AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_create(ctx, repo.id, user_id, params).await
}
pub async fn git_release_update_by_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
id: Uuid,
params: UpdateRelease,
) -> Result<ReleaseResponse, AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_update(repo.id, id, params).await
}
pub async fn git_release_delete_by_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
id: Uuid,
) -> Result<(), AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_delete(repo.id, id).await
}
pub async fn git_release_delete_by_tag_name(
&self,
ctx: &Session,
_user_id: Uuid,
wk: &str,
repo: &str,
tag: &str,
) -> Result<(), AppError> {
let repo = self.git_require_member(ctx, wk, repo).await?;
self.git_release_delete_by_tag(repo.id, tag).await
}
}
impl AppService {
async fn default_branch_sha(
&self,
repo_id: Uuid,
) -> Result<String, AppError> {
sqlx::query_scalar("SELECT target_sha FROM repo_ref WHERE repo = $1 AND is_default = true")
.bind(repo_id)
.fetch_optional(self.db.reader())
.await
.map_err(|e| AppError::DatabaseError(e.to_string()))?
.ok_or_else(|| AppError::BadRequest("no default branch to target".to_string()))
}
}
fn release_to_response(
r: RepoReleaseModel,
assets: Vec<ReleaseAssetResponse>,
) -> ReleaseResponse {
ReleaseResponse {
id: r.id,
tag_name: r.tag_name,
target_commit_sha: r.target_commit_sha,
name: r.name,
body: r.body,
draft: r.draft,
prerelease: r.prerelease,
author: r.author,
assets,
published_at: r.published_at,
created_at: r.created_at,
}
}