feat: add the ability to resync props

This commit is contained in:
Evan Carroll 2026-01-15 19:02:34 -06:00
parent 8447fdef5d
commit e57323ff3f
5 changed files with 180 additions and 36 deletions

View file

@ -1,8 +1,9 @@
//! Props management API handlers for admin UI.
use axum::extract::State;
use axum::extract::{Query, State};
use axum::Json;
use axum_extra::extract::Multipart;
use serde::Deserialize;
use chattyness_db::{
models::{CreateServerPropRequest, ServerProp, ServerPropSummary},
queries::props,
@ -18,6 +19,14 @@ use uuid::Uuid;
// API Types
// =============================================================================
/// Query parameters for prop creation.
#[derive(Debug, Deserialize)]
pub struct CreatePropQuery {
/// If true, update existing prop with same slug instead of returning 409 Conflict.
#[serde(default)]
pub force: bool,
}
/// Response for prop creation.
#[derive(Debug, Serialize)]
pub struct CreatePropResponse {
@ -102,8 +111,12 @@ pub async fn list_props(State(pool): State<PgPool>) -> Result<Json<Vec<ServerPro
/// Expects multipart form with:
/// - `metadata`: JSON object with prop details (CreateServerPropRequest)
/// - `file`: Binary SVG or PNG file
///
/// Query parameters:
/// - `force`: If true, update existing prop with same slug instead of returning 409 Conflict.
pub async fn create_prop(
State(pool): State<PgPool>,
Query(query): Query<CreatePropQuery>,
mut multipart: Multipart,
) -> Result<Json<CreatePropResponse>, AppError> {
let mut metadata: Option<CreateServerPropRequest> = None;
@ -165,23 +178,27 @@ pub async fn create_prop(
// Validate the request
metadata.validate()?;
// Check slug availability
let slug = metadata.slug_or_generate();
let available = props::is_prop_slug_available(&pool, &slug).await?;
if !available {
return Err(AppError::Conflict(format!(
"Prop slug '{}' is already taken",
slug
)));
}
// Store the file
// Store the file first (SHA256-based, safe to run even if prop exists)
let asset_path = store_prop_file(&file_bytes, &extension).await?;
// Create the prop in database
let prop = props::create_server_prop(&pool, &metadata, &asset_path, None).await?;
let prop = if query.force {
// Force mode: upsert (insert or update)
props::upsert_server_prop(&pool, &metadata, &asset_path, None).await?
} else {
// Normal mode: check availability first
let slug = metadata.slug_or_generate();
let available = props::is_prop_slug_available(&pool, &slug).await?;
if !available {
return Err(AppError::Conflict(format!(
"Prop slug '{}' is already taken",
slug
)));
}
props::create_server_prop(&pool, &metadata, &asset_path, None).await?
};
tracing::info!("Created server prop: {} ({})", prop.name, prop.id);
let action = if query.force { "Upserted" } else { "Created" };
tracing::info!("{} server prop: {} ({})", action, prop.name, prop.id);
Ok(Json(CreatePropResponse::from(prop)))
}