athena_rs 0.76.0

WIP Database API gateway
Documentation
//! Delete gateway helpers that remove records via Supabase and optionally publish audit events.
//!
//! The `/gateway/delete` route validates headers, routes to the configured Supabase client, and
//! ensures downstream services receive the generated audit events.
use actix_web::{
    HttpRequest, HttpResponse, delete,
    http::StatusCode,
    web::{Data, Json},
};
use serde::Deserialize;
use serde_json::json;
use std::time::Instant;
use supabase_rs::SupabaseClient;

use crate::AppState;
use crate::api::headers::x_athena_client::x_athena_client;
use crate::api::headers::x_company_id::get_x_company_id;
use crate::api::headers::x_organization_id::get_x_organization_id;
use crate::api::headers::x_publish_event::get_x_publish_event;
use crate::api::headers::x_user_id::get_x_user_id;
use crate::data::events::post_event;
use crate::data::suitsbooks_supabase;
use crate::utils::request_logging::{log_operation_event, log_request};

#[derive(Debug, Deserialize)]
/// Body received by `/gateway/delete` describing which row to delete.
struct DeleteRequest {
    /// The Supabase table that should be cleaned up.
    table_name: String,
    /// The resource identifier of the row to delete (column varies per table).
    resource_id: String,
}

#[delete("/gateway/delete")]
/// Deletes a single record from the requested table after validating Athena headers.
///
/// # Parameters
/// - `req`: Headers drive which client (Postgres/Supabase/custom) to use and supply user/organization context.
/// - `body`: JSON body describing `table_name` and `resource_id`.
///
/// # Headers
/// - `X-Athena-Client` selects `custom_supabase` or the default Supabase client.
/// - `X-User-Id`, `X-Company-Id`, and `X-Organization-Id` must be present or resolved via `apikey`.
/// - Optional `x-supabase-url`/`x-supabase-key` override the default Supabase endpoint.
///
/// # Returns
/// - `200 OK` with success metadata when the delete succeeds.
/// - `500 Internal Server Error` on Supabase failures.
/// - `400 Bad Request` when required headers or body fields are missing.
///
/// # Example
/// ```http
/// DELETE /gateway/delete
/// X-Athena-Client: custom_supabase
/// X-User-Id: user-1
/// X-Company-Id: comp-1
/// X-Organization-Id: org-1
/// Content-Type: application/json
///
/// {
///   "table_name": "users",
///   "resource_id": "user-123"
/// }
/// ```
pub async fn delete_data(
    req: HttpRequest,
    body: Json<DeleteRequest>,
    app_state: Data<AppState>,
) -> HttpResponse {
    let logged_request = log_request(req.clone(), Some(app_state.get_ref()));
    let operation_start = Instant::now();
    let client_name: String = x_athena_client(&req.clone());

    let _user_id: String = match get_x_user_id(&req) {
        Some(id) => id,
        None => {
            return HttpResponse::BadRequest()
                .json(json!({"error": "X-User-Id header not found in the request"}));
        }
    };

    let _company_id: String = match get_x_company_id(&req) {
        Some(id) => id,
        None => {
            return HttpResponse::BadRequest()
                .json(json!({"error": "X-Company-Id header not found in the request"}));
        }
    };

    let _organization_id: String = match get_x_organization_id(&req) {
        Some(id) => id,
        None => {
            return HttpResponse::BadRequest()
                .json(json!({"error": "X-Organization-Id header not found in the request"}));
        }
    };

    let table_name: String = body.table_name.clone();
    let resource_id: String = body.resource_id.clone();

    let client: SupabaseClient = if x_athena_client(&req.clone()) == "custom_supabase" {
        let supabase_url: Option<&str> = req
            .headers()
            .get("x-supabase-url")
            .and_then(|v| v.to_str().ok());
        let supabase_key = req
            .headers()
            .get("x-supabase-key")
            .and_then(|v| v.to_str().ok());
        match (supabase_url, supabase_key) {
            (Some(url), Some(key)) => SupabaseClient::new(url.to_string(), key.to_string())
                .unwrap_or_else(|_| futures::executor::block_on(suitsbooks_supabase())),
            _ => suitsbooks_supabase().await,
        }
    } else {
        suitsbooks_supabase().await
    };
    let result: Result<(), String> = client.delete(&table_name, &resource_id).await;

    match result {
        Ok(_) => {
            if get_x_publish_event(&req) {
                let event: serde_json::Value = json!({
                    "event": "DELETE",
                    "resource": table_name.clone(),
                    "resource_id": resource_id.clone(),
                });
                post_event(_company_id, event).await;
            }

            log_operation_event(
                Some(app_state.get_ref()),
                &logged_request,
                "delete",
                Some(&table_name),
                operation_start.elapsed().as_millis(),
                StatusCode::OK,
                Some(json!({
                    "resource_id": resource_id.clone(),
                })),
            );

            HttpResponse::Ok().json(json!({
                "status": "success",
                "success": true,
                "message": "Data deleted successfully",
                "table_name": table_name,
                "resource_id": resource_id,
                "client": client_name,
            }))
        }
        Err(err) => {
            log_operation_event(
                Some(app_state.get_ref()),
                &logged_request,
                "delete",
                Some(&table_name),
                operation_start.elapsed().as_millis(),
                StatusCode::INTERNAL_SERVER_ERROR,
                Some(json!({
                    "error": err,
                })),
            );

            HttpResponse::InternalServerError().json(json!({
                "status": "error",
                "success": false,
                "message": format!("Failed to delete: {:?}", err),
                "table_name": table_name,
                "resource_id": resource_id,
            }))
        }
    }
}