//! Version management handlers for items. use axum::{ extract::{Path, State}, response::IntoResponse, Json, }; use serde::{Deserialize, Serialize}; use crate::{ auth::AuthUser, db::{self, ItemId, VersionId}, error::{AppError, Result}, types::ListResponse, validation, AppState, }; use super::super::verify_item_ownership; /// JSON input for creating a new version of an item. #[derive(Debug, Deserialize)] pub struct CreateVersionRequest { pub version_number: String, pub changelog: Option, pub file_url: Option, pub file_size_bytes: Option, pub file_name: Option, pub label: Option, } /// JSON response representing an item version. #[derive(Debug, Serialize)] pub struct VersionResponse { pub id: VersionId, pub item_id: ItemId, pub version_number: String, pub changelog: Option, pub file_url: Option, pub download_count: i32, pub is_current: bool, } /// Create a new version for an owned item. #[tracing::instrument(skip_all, name = "items::create_version")] pub(in crate::routes::api) async fn create_version( State(state): State, AuthUser(user): AuthUser, Path(item_id): Path, Json(req): Json, ) -> Result { user.check_not_suspended()?; // Validate input validation::validate_version_number(&req.version_number)?; if let Some(ref changelog) = req.changelog { validation::validate_changelog(changelog)?; } let (item, _) = verify_item_ownership(&state, item_id, user.id).await?; let version = db::versions::create_version( &state.db, item_id, &req.version_number, req.changelog.as_deref(), req.file_url.as_deref(), req.file_size_bytes, req.file_name.as_deref(), req.label.as_deref(), ) .await?; db::projects::bump_cache_generation(&state.db, item.project_id).await?; Ok(Json(VersionResponse { id: version.id, item_id: version.item_id, version_number: version.version_number, changelog: version.changelog, file_url: version.file_url, download_count: version.download_count, is_current: version.is_current, })) } /// Delete a version from an owned item. Enqueues S3 deletion if file exists. #[tracing::instrument(skip_all, name = "items::delete_version")] pub(in crate::routes::api) async fn delete_version( State(state): State, AuthUser(user): AuthUser, Path((item_id, version_id)): Path<(ItemId, VersionId)>, ) -> Result { user.check_not_suspended()?; verify_item_ownership(&state, item_id, user.id).await?; let version = db::versions::get_version_by_id(&state.db, version_id) .await? .ok_or(AppError::NotFound)?; if version.item_id != item_id { return Err(AppError::NotFound); } // delete_version handles storage decrement + S3 enqueue atomically let _ = version; db::versions::delete_version(&state.db, version_id).await?; Ok(axum::http::StatusCode::OK) } /// List all versions for a given item (public items only; drafts return 404). #[tracing::instrument(skip_all, name = "items::list_versions")] pub(in crate::routes::api) async fn list_versions( State(state): State, Path(item_id): Path, ) -> Result { let item = db::items::get_item_by_id(&state.db, item_id) .await? .ok_or(AppError::NotFound)?; if !item.is_public { return Err(AppError::NotFound); } let versions = db::versions::get_versions_by_item(&state.db, item_id).await?; let data: Vec = versions .into_iter() .map(|v| VersionResponse { id: v.id, item_id: v.item_id, version_number: v.version_number, changelog: v.changelog, file_url: v.file_url, download_count: v.download_count, is_current: v.is_current, }) .collect(); Ok(Json(ListResponse { data })) }