summaryrefslogblamecommitdiff
path: root/makima/src/server/handlers/repository_history.rs
blob: 9c309c0217fc582bd6cb532081693d23f681faf7 (plain) (tree)


































































































                                                                                                                 







                                          









                                                 










                                                             































































                                                                                   
//! HTTP handlers for repository history management.
//! Provides endpoints for listing, suggesting, and deleting repository history entries.

use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};
use uuid::Uuid;

use crate::db::models::{RepositoryHistoryListResponse, RepositorySuggestionsQuery};
use crate::db::repository;
use crate::server::auth::Authenticated;
use crate::server::messages::ApiError;
use crate::server::state::SharedState;

/// List all repository history entries for the authenticated user.
/// Returns entries ordered by use_count DESC, last_used_at DESC.
#[utoipa::path(
    get,
    path = "/api/v1/settings/repository-history",
    responses(
        (status = 200, description = "List of repository history entries", body = RepositoryHistoryListResponse),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Settings"
)]
pub async fn list_repository_history(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::list_repository_history_for_owner(pool, auth.owner_id).await {
        Ok(entries) => {
            let total = entries.len() as i64;
            Json(RepositoryHistoryListResponse { entries, total }).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to list repository history: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Get repository suggestions based on history.
/// Optionally filter by source_type (remote/local) and search query.
#[utoipa::path(
    get,
    path = "/api/v1/settings/repository-history/suggestions",
    params(
        ("source_type" = Option<String>, Query, description = "Filter by source type: 'remote' or 'local'"),
        ("query" = Option<String>, Query, description = "Search query to filter by name or URL/path"),
        ("limit" = Option<i32>, Query, description = "Limit results (default: 10)")
    ),
    responses(
        (status = 200, description = "List of repository suggestions", body = RepositoryHistoryListResponse),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Settings"
)]
pub async fn get_repository_suggestions(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Query(params): Query<RepositorySuggestionsQuery>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    let limit = params.limit.unwrap_or(10).min(50); // Cap at 50 for safety

    tracing::debug!(
        owner_id = %auth.owner_id,
        source_type = ?params.source_type,
        query = ?params.query,
        limit = limit,
        "Fetching repository suggestions"
    );

    match repository::get_repository_suggestions(
        pool,
        auth.owner_id,
        params.source_type.as_deref(),
        params.query.as_deref(),
        limit,
    )
    .await
    {
        Ok(entries) => {
            // Debug log to help diagnose filtering issues
            for entry in &entries {
                tracing::debug!(
                    id = %entry.id,
                    name = %entry.name,
                    source_type = %entry.source_type,
                    has_url = entry.repository_url.is_some(),
                    has_path = entry.local_path.is_some(),
                    "Repository suggestion entry"
                );
            }
            let total = entries.len() as i64;
            Json(RepositoryHistoryListResponse { entries, total }).into_response()
        }
        Err(e) => {
            tracing::error!("Failed to get repository suggestions: {}", e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}

/// Delete a repository history entry.
#[utoipa::path(
    delete,
    path = "/api/v1/settings/repository-history/{id}",
    params(
        ("id" = Uuid, Path, description = "Repository history entry ID")
    ),
    responses(
        (status = 204, description = "Entry deleted"),
        (status = 401, description = "Unauthorized", body = ApiError),
        (status = 404, description = "Entry not found", body = ApiError),
        (status = 503, description = "Database not configured", body = ApiError),
        (status = 500, description = "Internal server error", body = ApiError),
    ),
    security(
        ("bearer_auth" = []),
        ("api_key" = [])
    ),
    tag = "Settings"
)]
pub async fn delete_repository_history(
    State(state): State<SharedState>,
    Authenticated(auth): Authenticated,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let Some(ref pool) = state.db_pool else {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
        )
            .into_response();
    };

    match repository::delete_repository_history(pool, id, auth.owner_id).await {
        Ok(true) => StatusCode::NO_CONTENT.into_response(),
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiError::new("NOT_FOUND", "Repository history entry not found")),
        )
            .into_response(),
        Err(e) => {
            tracing::error!("Failed to delete repository history {}: {}", id, e);
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiError::new("DB_ERROR", e.to_string())),
            )
                .into_response()
        }
    }
}