use axum::extract::State;
use axum::http::header::CONTENT_TYPE;
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use sbol_db_core::JobId;
use sbol_db_storage::{ListJobsFilter, NewJob, SbolJob};
use serde::Deserialize;
use serde_json::{json, Value};
use super::auth::parse_body;
use super::CurrentUser;
use crate::error::ApiError;
use crate::AppState;
const JOB_LIST_LIMIT: u32 = 100;
fn job_json(job: &SbolJob) -> Value {
json!({
"id": job.id.to_string(),
"kind": job.kind,
"status": format!("{:?}", job.status).to_lowercase(),
"queue": job.queue,
"attempts": job.attempts,
"maxAttempts": job.max_attempts,
"error": job.error,
"createdAt": job.created_at.to_rfc3339(),
"startedAt": job.started_at.map(|t| t.to_rfc3339()),
"finishedAt": job.finished_at.map(|t| t.to_rfc3339()),
})
}
async fn list_recent(state: &AppState) -> Result<Vec<Value>, ApiError> {
let filter = ListJobsFilter {
kind: None,
status: None,
queue: None,
correlation_id: None,
since: None,
limit: JOB_LIST_LIMIT,
};
let jobs = state.jobs.list(&filter).await?;
Ok(jobs.iter().map(job_json).collect())
}
pub async fn admin_jobs(State(state): State<AppState>) -> Result<Response, ApiError> {
Ok(Json(list_recent(&state).await?).into_response())
}
#[derive(Debug, Deserialize)]
struct JobAction {
#[serde(alias = "jobId", alias = "job")]
id: JobId,
}
pub async fn cancel_job(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, ApiError> {
if user.is_none() {
return Ok(unauthorized());
}
let action: JobAction = parse_body(&headers, &body)?;
let cancelled = state.jobs.cancel(action.id).await?;
Ok(Json(json!({ "cancelled": cancelled })).into_response())
}
pub async fn restart_job(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, ApiError> {
if user.is_none() {
return Ok(unauthorized());
}
let action: JobAction = parse_body(&headers, &body)?;
let Some(job) = state.jobs.get(action.id).await? else {
return Err(ApiError::NotFound(format!("job {}", action.id)));
};
let outcome = state
.jobs
.enqueue(NewJob {
kind: job.kind,
payload: job.payload,
queue: Some(job.queue),
priority: Some(job.priority),
max_attempts: Some(job.max_attempts),
idempotency_key: None,
available_at: None,
parent_job_id: None,
correlation_id: None,
})
.await?;
let id = match outcome {
sbol_db_storage::EnqueueOutcome::Inserted(job)
| sbol_db_storage::EnqueueOutcome::AlreadyExists(job) => job.id,
};
Ok(Json(json!({ "jobId": id.to_string() })).into_response())
}
pub async fn virtuoso() -> Response {
Json(json!({ "engine": "native", "virtuoso": false })).into_response()
}
pub async fn list_logs() -> Response {
Json(json!({ "logs": [], "source": "job-log" })).into_response()
}
pub async fn corrupt_log() -> Response {
Json(json!({ "entries": [] })).into_response()
}
fn unauthorized() -> Response {
(
axum::http::StatusCode::UNAUTHORIZED,
[(CONTENT_TYPE, "text/plain")],
"authentication required",
)
.into_response()
}