use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, }; use common::models::{CreateLibraryBlockPayload, LibraryBlock, UpdateLibraryBlockPayload}; use common::{auth::Claims, middleware::Org}; use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; #[derive(Debug, Deserialize)] pub struct LibraryBlockFilters { #[serde(rename = "type")] pub block_type: Option, pub tags: Option, // Comma-separated list pub search: Option, } /// POST /api/library/blocks - Guardar un bloque en la biblioteca pub async fn create_library_block( Org(org_ctx): Org, claims: Claims, State(pool): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { let block = sqlx::query_as!( LibraryBlock, r#" INSERT INTO library_blocks (organization_id, created_by, name, description, block_type, block_data, tags) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at "#, org_ctx.id, claims.sub, payload.name, payload.description, payload.block_type, payload.block_data, payload.tags.as_deref() ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(block)) } /// GET /api/library/blocks - Listar bloques de la biblioteca pub async fn list_library_blocks( Org(org_ctx): Org, State(pool): State, Query(filters): Query, ) -> Result>, (StatusCode, String)> { // Base query let mut query = String::from("SELECT * FROM library_blocks WHERE organization_id = $1"); let mut param_count = 1; // Filtro por tipo if filters.block_type.is_some() { param_count += 1; query.push_str(&format!(" AND block_type = ${}", param_count)); } // Filtro por tags (busca si algún tag coincide) if filters.tags.is_some() { param_count += 1; query.push_str(&format!(" AND tags && ${}", param_count)); } // Búsqueda en nombre y descripción if filters.search.is_some() { param_count += 1; query.push_str(&format!( " AND (name ILIKE ${0} OR description ILIKE ${0})", param_count )); } query.push_str(" ORDER BY created_at DESC"); // Build query con bind dinámico let mut sql_query = sqlx::query_as::<_, LibraryBlock>(&query).bind(org_ctx.id); if let Some(block_type) = &filters.block_type { sql_query = sql_query.bind(block_type); } if let Some(tags_str) = &filters.tags { let tags: Vec = tags_str.split(',').map(|s| s.trim().to_string()).collect(); sql_query = sql_query.bind(tags); } let search_pattern = filters.search.as_ref().map(|s| format!("%{}%", s)); if let Some(ref pattern) = search_pattern { sql_query = sql_query.bind(pattern); } let blocks = sql_query .fetch_all(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(Json(blocks)) } /// GET /api/library/blocks/:id - Obtener un bloque específico pub async fn get_library_block( Org(org_ctx): Org, State(pool): State, Path(block_id): Path, ) -> Result, (StatusCode, String)> { let block = sqlx::query_as!( LibraryBlock, r#"SELECT id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at FROM library_blocks WHERE id = $1 AND organization_id = $2"#, block_id, org_ctx.id ) .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; match block { Some(b) => Ok(Json(b)), None => Err((StatusCode::NOT_FOUND, "Block not found".to_string())), } } /// PUT /api/library/blocks/:id - Actualizar bloque (nombre, descripción, tags) pub async fn update_library_block( Org(org_ctx): Org, State(pool): State, Path(block_id): Path, Json(payload): Json, ) -> Result, (StatusCode, String)> { // Verificar que el bloque existe y pertenece a la org let existing = sqlx::query!( "SELECT id FROM library_blocks WHERE id = $1 AND organization_id = $2", block_id, org_ctx.id ) .fetch_optional(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if existing.is_none() { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); } // Update dinámico basado en campos provistos let updated = if let Some(name) = &payload.name { sqlx::query_as!( LibraryBlock, r#" UPDATE library_blocks SET name = COALESCE($1, name), description = COALESCE($2, description), tags = COALESCE($3, tags), updated_at = NOW() WHERE id = $4 AND organization_id = $5 RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at "#, Some(name), payload.description, payload.tags.as_deref(), block_id, org_ctx.id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? } else { sqlx::query_as!( LibraryBlock, r#" UPDATE library_blocks SET description = COALESCE($1, description), tags = COALESCE($2, tags), updated_at = NOW() WHERE id = $3 AND organization_id = $4 RETURNING id, organization_id, created_by, name, description, block_type, block_data, tags, usage_count as "usage_count!", created_at, updated_at "#, payload.description, payload.tags.as_deref(), block_id, org_ctx.id ) .fetch_one(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? }; Ok(Json(updated)) } /// DELETE /api/library/blocks/:id - Eliminar bloque pub async fn delete_library_block( Org(org_ctx): Org, State(pool): State, Path(block_id): Path, ) -> Result { let result = sqlx::query!( "DELETE FROM library_blocks WHERE id = $1 AND organization_id = $2", block_id, org_ctx.id ) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); } Ok(StatusCode::NO_CONTENT) } /// POST /api/library/blocks/:id/increment-usage - Incrementar contador de uso pub async fn increment_block_usage( Org(org_ctx): Org, State(pool): State, Path(block_id): Path, ) -> Result { let result = sqlx::query!( "UPDATE library_blocks SET usage_count = usage_count + 1 WHERE id = $1 AND organization_id = $2", block_id, org_ctx.id ) .execute(&pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Block not found".to_string())); } Ok(StatusCode::OK) }