Initial commit
This commit is contained in:
368
backend/src/handlers_sync.rs
Executable file
368
backend/src/handlers_sync.rs
Executable file
@@ -0,0 +1,368 @@
|
||||
use crate::state::AppState;
|
||||
use crate::sync::*;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use postgres::Row;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use crate::errors::ApiError;
|
||||
use tracing::{info, error};
|
||||
|
||||
pub async fn sync_status(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Get sync status");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Get stats
|
||||
let stats_result = conn.query_one(
|
||||
"SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'running'::sync_job_status) AS running,
|
||||
COUNT(*) FILTER (WHERE status = 'completed'::sync_job_status AND created_at >= CURRENT_DATE) AS completed_today,
|
||||
COUNT(*) FILTER (WHERE status = 'failed'::sync_job_status AND created_at >= CURRENT_DATE) AS failed_today
|
||||
FROM sync_jobs",
|
||||
&[],
|
||||
);
|
||||
|
||||
let (running, completed_today, failed_today) = match stats_result {
|
||||
Ok(row) => (
|
||||
row.get::<_, i64>(0),
|
||||
row.get::<_, i64>(1),
|
||||
row.get::<_, i64>(2),
|
||||
),
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Stats query failed");
|
||||
(0, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
// Get recent jobs
|
||||
let recent_result = match conn.query(
|
||||
"SELECT id, job_type, sync_direction, status::text, records_processed, records_failed, created_at::text, started_at::text, completed_at::text
|
||||
FROM sync_jobs ORDER BY created_at DESC LIMIT 5",
|
||||
&[],
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let recent_jobs: Vec<_> = recent_result
|
||||
.into_iter()
|
||||
.map(|row| job_to_json(&row))
|
||||
.collect();
|
||||
|
||||
let response = json!({
|
||||
"is_running": running > 0,
|
||||
"current_job": recent_jobs.iter().find(|job| job["status"] == "running").cloned(),
|
||||
"recent_jobs": recent_jobs,
|
||||
"stats": {
|
||||
"running": running,
|
||||
"completed_today": completed_today,
|
||||
"failed_today": failed_today
|
||||
}
|
||||
});
|
||||
|
||||
Ok((StatusCode::OK, Json(response)).into_response())
|
||||
}
|
||||
|
||||
pub async fn sync_jobs(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Get sync jobs");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let result = conn.query(
|
||||
"SELECT id, job_type, sync_direction, status::text, records_processed, records_failed, created_at::text, started_at::text, completed_at::text
|
||||
FROM sync_jobs ORDER BY created_at DESC LIMIT 20",
|
||||
&[],
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let jobs: Vec<_> = rows.into_iter().map(|r| job_to_json(&r)).collect();
|
||||
Ok((StatusCode::OK, Json(json!({ "jobs": jobs }))).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Database error");
|
||||
Err(ApiError::Database(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_sync(
|
||||
State(state): State<Arc<AppState>>,
|
||||
req: SyncStartRequest,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, job_type = %req.job_type, direction = %req.sync_direction, "Start sync");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match &req.session_id {
|
||||
Some(session_id) => {
|
||||
match conn.query_opt(
|
||||
"SELECT user_id FROM sessions WHERE id = $1 AND expires_at > CURRENT_TIMESTAMP",
|
||||
&[&session_id],
|
||||
) {
|
||||
Ok(Some(row)) => row.get::<_, i32>(0),
|
||||
Ok(None) => {
|
||||
error!(request_id = %request_id, session_id = %session_id, "Session not found");
|
||||
return Err(ApiError::Authentication("Session not found or expired".to_string()));
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Session query error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!(request_id = %request_id, "No session ID provided");
|
||||
return Err(ApiError::Authentication("No session ID provided".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match conn.execute(
|
||||
"INSERT INTO sync_jobs (job_type, sync_direction, status, created_by, created_at) VALUES ($1, $2, 'pending'::sync_job_status, $3, NOW())",
|
||||
&[&req.job_type, &req.sync_direction, &user_id],
|
||||
) {
|
||||
Ok(_) => {
|
||||
info!(request_id = %request_id, "Sync job created");
|
||||
Ok((StatusCode::OK, Json(json!({
|
||||
"message": "Sync job started",
|
||||
"job_type": req.job_type,
|
||||
"direction": req.sync_direction
|
||||
}))).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Failed to create sync job");
|
||||
Err(ApiError::Database(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_sync(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Stop sync");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match conn.execute(
|
||||
"UPDATE sync_jobs SET status = 'cancelled'::sync_job_status, completed_at = NOW() WHERE status IN ('running'::sync_job_status, 'pending'::sync_job_status)",
|
||||
&[],
|
||||
) {
|
||||
Ok(_) => {
|
||||
info!(request_id = %request_id, "Sync jobs stopped");
|
||||
Ok((StatusCode::OK, Json(json!({ "message": "Sync jobs stopped" }))).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Failed to stop sync jobs");
|
||||
Err(ApiError::Database(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn simulate_sync(
|
||||
State(state): State<Arc<AppState>>,
|
||||
data: serde_json::Value,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Simulate sync");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut items: Vec<SyncItem> = Vec::new();
|
||||
let data_type = data.get("data_type").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
|
||||
// Fetch customers from database
|
||||
if data_type == "customers" {
|
||||
let rows = match conn.query(
|
||||
"SELECT sap_card_code, plesk_customer_id, plesk_subscription_id FROM customers",
|
||||
&[],
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Database error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let sap_code: String = row.get(0);
|
||||
let status = if i % 3 == 0 { "new" } else if i % 3 == 1 { "update" } else { "unchanged" };
|
||||
items.push(SyncItem {
|
||||
id: format!("sim-{}", i),
|
||||
source_id: sap_code.clone(),
|
||||
target_id: if status == "new" { None } else { Some(format!("PLESK-{}", 2000 + i)) },
|
||||
name: format!("Customer {}", sap_code),
|
||||
status: status.to_string(),
|
||||
source_data: json!({"sap_card_code": sap_code}),
|
||||
target_data: if status == "new" { None } else { Some(json!({"plesk_id": 2000 + i})) },
|
||||
diff: None,
|
||||
});
|
||||
}
|
||||
} else if data_type == "domains" {
|
||||
// Simulate domain data
|
||||
for i in 0..10 {
|
||||
let status = if i % 3 == 0 { "new" } else if i % 3 == 1 { "update" } else { "unchanged" };
|
||||
items.push(SyncItem {
|
||||
id: format!("sim-domain-{}", i),
|
||||
source_id: format!("SAP-DOM-{}", 1000 + i),
|
||||
target_id: if status == "new" { None } else { Some(format!("PLESK-DOM-{}", i)) },
|
||||
name: format!("domain{}.example.com", i),
|
||||
status: status.to_string(),
|
||||
source_data: json!({"domain_id": 1000 + i}),
|
||||
target_data: if status == "new" { None } else { Some(json!({"plesk_domain_id": i})) },
|
||||
diff: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let direction = data.get("direction").and_then(|v| v.as_str()).unwrap_or("sap_to_plesk");
|
||||
let result = SimulationResult {
|
||||
data_type: data_type.to_string(),
|
||||
direction: direction.to_string(),
|
||||
total_records: items.len(),
|
||||
new: items.iter().filter(|item| item.status == "new").count(),
|
||||
updated: items.iter().filter(|item| item.status == "update").count(),
|
||||
conflicts: items.iter().filter(|item| item.status == "conflict").count(),
|
||||
unchanged: items.iter().filter(|item| item.status == "unchanged").count(),
|
||||
deleted: 0,
|
||||
items,
|
||||
};
|
||||
|
||||
Ok((StatusCode::OK, Json(json!(result))).into_response())
|
||||
}
|
||||
|
||||
pub async fn get_conflicts(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Get conflicts");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let result = conn.query(
|
||||
"SELECT id, sync_job_id, entity_type, entity_id, resolution_status, source_data, target_data, conflict_details FROM sync_conflicts ORDER BY created_at DESC LIMIT 20",
|
||||
&[],
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let conflicts: Vec<Conflict> = rows
|
||||
.into_iter()
|
||||
.map(|row| Conflict {
|
||||
id: row.get::<_, i32>(0),
|
||||
sync_job_id: row.get::<_, i32>(1),
|
||||
entity_type: row.get::<_, String>(2),
|
||||
entity_id: row.get::<_, String>(3),
|
||||
resolution_status: row.get::<_, String>(4),
|
||||
source_data: row.get::<_, Option<String>>(5).unwrap_or_default(),
|
||||
target_data: row.get::<_, Option<String>>(6),
|
||||
conflict_details: row.get::<_, Option<String>>(7),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((StatusCode::OK, Json(json!({ "conflicts": conflicts }))).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Database error");
|
||||
Err(ApiError::Database(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_conflict(
|
||||
State(state): State<Arc<AppState>>,
|
||||
req: ConflictResolution,
|
||||
) -> impl IntoResponse {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
info!(request_id = %request_id, "Resolve conflict");
|
||||
|
||||
let mut conn = match state.pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, "Database connection error");
|
||||
return Err(ApiError::Database(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match conn.execute(
|
||||
"UPDATE sync_conflicts SET resolution_status = $1, resolved_data = $2::jsonb WHERE id = $3",
|
||||
&[&req.action, &req.resolved_data.to_string(), &req.id],
|
||||
) {
|
||||
Ok(_) => {
|
||||
info!(request_id = %request_id, "Conflict resolved");
|
||||
Ok((StatusCode::OK, Json(json!({ "message": "Conflict resolved" }))).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(request_id = %request_id, error = %e, "Failed to resolve conflict");
|
||||
Err(ApiError::Database(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn job_to_json(row: &Row) -> serde_json::Value {
|
||||
json!({
|
||||
"id": row.get::<_, i32>(0),
|
||||
"job_type": row.get::<_, String>(1),
|
||||
"sync_direction": row.get::<_, String>(2),
|
||||
"status": row.get::<_, String>(3),
|
||||
"records_processed": row.get::<_, i32>(4),
|
||||
"records_failed": row.get::<_, i32>(5),
|
||||
"created_at": row.get::<_, String>(6),
|
||||
"started_at": row.get::<_, Option<String>>(7),
|
||||
"completed_at": row.get::<_, Option<String>>(8),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user