use axum::extract::{Json, Path, Query, State};
use crate::db::{DbPool, models::*};
use crate::error::LificError;
use super::{with_read, with_write};
pub(super) async fn list_issues(
State(db): State<DbPool>,
Query(q): Query<ListIssuesQuery>,
) -> Result<Json<Vec<Issue>>, LificError> {
with_read(&db, |conn| crate::db::queries::list_issues(conn, &q)).map(Json)
}
pub(super) async fn get_issue(
State(db): State<DbPool>,
Path(id): Path<i64>,
) -> Result<Json<Issue>, LificError> {
with_read(&db, |conn| crate::db::queries::get_issue(conn, id)).map(Json)
}
pub(super) async fn resolve_issue(
State(db): State<DbPool>,
Path(identifier): Path<String>,
) -> Result<Json<Issue>, LificError> {
with_read(&db, |conn| {
let id = crate::db::queries::resolve_identifier(conn, &identifier)?;
crate::db::queries::get_issue(conn, id)
})
.map(Json)
}
pub(super) async fn create_issue(
State(db): State<DbPool>,
Json(input): Json<CreateIssue>,
) -> Result<Json<Issue>, LificError> {
with_write(&db, |conn| crate::db::queries::create_issue(conn, &input)).map(Json)
}
pub(super) async fn update_issue(
State(db): State<DbPool>,
Path(id): Path<i64>,
Json(input): Json<UpdateIssue>,
) -> Result<Json<Issue>, LificError> {
with_write(&db, |conn| {
crate::db::queries::update_issue(conn, id, &input)
})
.map(Json)
}
pub(super) async fn delete_issue_handler(
State(db): State<DbPool>,
Path(id): Path<i64>,
) -> Result<Json<serde_json::Value>, LificError> {
with_write(&db, |conn| crate::db::queries::delete_issue(conn, id))?;
Ok(Json(serde_json::json!({"deleted": true})))
}
#[derive(serde::Deserialize)]
pub(super) struct LinkRequest {
source: String,
target: String,
relation_type: String,
}
#[derive(serde::Deserialize)]
pub(super) struct UnlinkRequest {
source: String,
target: String,
}
pub(super) async fn link_issues(
State(db): State<DbPool>,
Json(input): Json<LinkRequest>,
) -> Result<Json<serde_json::Value>, LificError> {
with_write(&db, |conn| {
let source_id = crate::db::queries::resolve_identifier(conn, &input.source)?;
let target_id = crate::db::queries::resolve_identifier(conn, &input.target)?;
crate::db::queries::link_issues(conn, source_id, target_id, &input.relation_type)
})?;
Ok(Json(serde_json::json!({"linked": true})))
}
pub(super) async fn unlink_issues(
State(db): State<DbPool>,
Json(input): Json<UnlinkRequest>,
) -> Result<Json<serde_json::Value>, LificError> {
with_write(&db, |conn| {
let source_id = crate::db::queries::resolve_identifier(conn, &input.source)?;
let target_id = crate::db::queries::resolve_identifier(conn, &input.target)?;
crate::db::queries::unlink_issues(conn, source_id, target_id)
})?;
Ok(Json(serde_json::json!({"unlinked": true})))
}
#[cfg(test)]
mod tests {
use crate::api::test_helpers::*;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::test]
async fn issue_crud_lifecycle() {
let app = test_app();
let (project_id, _) = seed_project(&app).await;
let body = serde_json::json!({
"project_id": project_id,
"title": "Fix the bug",
"status": "todo",
"priority": "high"
});
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/issues")
.header("content-type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let issue: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let issue_id = issue["id"].as_i64().unwrap();
assert_eq!(issue["identifier"], "TST-1");
assert_eq!(issue["priority"], "high");
let resp = app
.clone()
.oneshot(
Request::builder()
.uri(format!("/api/issues?project_id={project_id}&status=todo"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let list: Vec<serde_json::Value> = serde_json::from_slice(&bytes).unwrap();
assert_eq!(list.len(), 1);
let update = serde_json::json!({"status": "active"});
let resp = app
.clone()
.oneshot(
Request::builder()
.method("PUT")
.uri(format!("/api/issues/{issue_id}"))
.header("content-type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&update).unwrap()))
.unwrap(),
)
.await
.unwrap();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let updated: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(updated["status"], "active");
let resp = app
.clone()
.oneshot(
Request::builder()
.uri("/api/issues/resolve/TST-1")
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri(format!("/api/issues/{issue_id}"))
.body(axum::body::Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}