feat: wrap-up REST API endpoints
This commit is contained in:
@@ -8,6 +8,7 @@ pub mod social;
|
||||
pub mod users;
|
||||
pub mod watchlist;
|
||||
pub mod webhook;
|
||||
pub mod wrapup;
|
||||
|
||||
pub use auth::*;
|
||||
pub use common::*;
|
||||
|
||||
30
crates/api-types/src/wrapup.rs
Normal file
30
crates/api-types/src/wrapup.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GenerateWrapUpRequest {
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub global: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct WrapUpGeneratedResponse {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct WrapUpStatusResponse {
|
||||
pub id: String,
|
||||
pub user_id: Option<String>,
|
||||
pub status: String,
|
||||
pub start_date: String,
|
||||
pub end_date: String,
|
||||
pub created_at: String,
|
||||
pub completed_at: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct WrapUpListResponse {
|
||||
pub items: Vec<WrapUpStatusResponse>,
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod images;
|
||||
pub mod import;
|
||||
pub mod rss;
|
||||
pub mod webhook;
|
||||
pub mod wrapup;
|
||||
|
||||
const DEFAULT_PAGE_LIMIT: u32 = 5;
|
||||
const RSS_FEED_LIMIT: u32 = 50;
|
||||
|
||||
147
crates/presentation/src/handlers/wrapup.rs
Normal file
147
crates/presentation/src/handlers/wrapup.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::wrapup::{
|
||||
commands::RequestWrapUpCommand,
|
||||
generate, get_wrapup,
|
||||
list_wrapups::{self, ListWrapUpsQuery},
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::wrapup::{WrapUpRecord, WrapUpStatus};
|
||||
use domain::value_objects::WrapUpId;
|
||||
|
||||
use crate::{errors::ApiError, extractors::AuthenticatedUser, state::AppState};
|
||||
use api_types::wrapup::{
|
||||
GenerateWrapUpRequest, WrapUpGeneratedResponse, WrapUpListResponse, WrapUpStatusResponse,
|
||||
};
|
||||
|
||||
fn record_to_dto(r: &WrapUpRecord) -> WrapUpStatusResponse {
|
||||
WrapUpStatusResponse {
|
||||
id: r.id.value().to_string(),
|
||||
user_id: r.user_id.map(|u| u.to_string()),
|
||||
status: format!("{:?}", r.status),
|
||||
start_date: r.start_date.to_string(),
|
||||
end_date: r.end_date.to_string(),
|
||||
created_at: r.created_at.to_string(),
|
||||
completed_at: r.completed_at.map(|t| t.to_string()),
|
||||
error_message: r.error_message.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/wrapups/generate",
|
||||
request_body = GenerateWrapUpRequest,
|
||||
responses(
|
||||
(status = 200, body = WrapUpGeneratedResponse),
|
||||
(status = 400, description = "Invalid date format"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn post_generate(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
Json(req): Json<GenerateWrapUpRequest>,
|
||||
) -> Result<Json<WrapUpGeneratedResponse>, ApiError> {
|
||||
let start = NaiveDate::parse_from_str(&req.start_date, "%Y-%m-%d")
|
||||
.map_err(|_| DomainError::ValidationError("invalid start_date".into()))?;
|
||||
let end = NaiveDate::parse_from_str(&req.end_date, "%Y-%m-%d")
|
||||
.map_err(|_| DomainError::ValidationError("invalid end_date".into()))?;
|
||||
let user_id = if req.global.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(user.0.value())
|
||||
};
|
||||
let cmd = RequestWrapUpCommand {
|
||||
user_id,
|
||||
start_date: start,
|
||||
end_date: end,
|
||||
};
|
||||
let id = generate::execute(&state.app_ctx, cmd).await?;
|
||||
Ok(Json(WrapUpGeneratedResponse {
|
||||
id: id.value().to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/wrapups",
|
||||
responses(
|
||||
(status = 200, body = WrapUpListResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_list(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<WrapUpListResponse>, ApiError> {
|
||||
let records = list_wrapups::execute(
|
||||
&state.app_ctx,
|
||||
ListWrapUpsQuery {
|
||||
user_id: Some(user.0.value()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(WrapUpListResponse {
|
||||
items: records.iter().map(record_to_dto).collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/wrapups/{id}",
|
||||
params(("id" = Uuid, Path, description = "Wrap-up ID")),
|
||||
responses(
|
||||
(status = 200, body = WrapUpStatusResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_status(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthenticatedUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<WrapUpStatusResponse>, ApiError> {
|
||||
let record = get_wrapup::execute(&state.app_ctx, WrapUpId::from_uuid(id))
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("wrap-up not found".into()))?;
|
||||
Ok(Json(record_to_dto(&record)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/wrapups/{id}/report",
|
||||
params(("id" = Uuid, Path, description = "Wrap-up ID")),
|
||||
responses(
|
||||
(status = 200, description = "Report JSON", content_type = "application/json"),
|
||||
(status = 202, description = "Still generating"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_report(
|
||||
State(state): State<AppState>,
|
||||
_user: AuthenticatedUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match get_wrapup::execute(&state.app_ctx, WrapUpId::from_uuid(id)).await {
|
||||
Ok(Some(record)) if record.status == WrapUpStatus::Ready => match record.report_json {
|
||||
Some(json) => (
|
||||
StatusCode::OK,
|
||||
[("content-type", "application/json")],
|
||||
json,
|
||||
)
|
||||
.into_response(),
|
||||
None => StatusCode::NOT_FOUND.into_response(),
|
||||
},
|
||||
Ok(Some(_)) => StatusCode::ACCEPTED.into_response(),
|
||||
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod social;
|
||||
mod users;
|
||||
mod watchlist;
|
||||
mod webhook;
|
||||
mod wrapup;
|
||||
|
||||
use axum::Router;
|
||||
use utoipa::{
|
||||
@@ -42,6 +43,7 @@ fn build() -> utoipa::openapi::OpenApi {
|
||||
api.merge(search::SearchDoc::openapi());
|
||||
api.merge(watchlist::WatchlistDoc::openapi());
|
||||
api.merge(webhook::WebhookDoc::openapi());
|
||||
api.merge(wrapup::WrapUpDoc::openapi());
|
||||
#[cfg(feature = "federation")]
|
||||
api.merge(social::SocialDoc::openapi());
|
||||
SecurityAddon.modify(&mut api);
|
||||
|
||||
18
crates/presentation/src/openapi/wrapup.rs
Normal file
18
crates/presentation/src/openapi/wrapup.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use utoipa::OpenApi;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::handlers::wrapup::post_generate,
|
||||
crate::handlers::wrapup::get_list,
|
||||
crate::handlers::wrapup::get_status,
|
||||
crate::handlers::wrapup::get_report,
|
||||
),
|
||||
components(schemas(
|
||||
api_types::wrapup::GenerateWrapUpRequest,
|
||||
api_types::wrapup::WrapUpGeneratedResponse,
|
||||
api_types::wrapup::WrapUpStatusResponse,
|
||||
api_types::wrapup::WrapUpListResponse,
|
||||
))
|
||||
)]
|
||||
pub struct WrapUpDoc;
|
||||
@@ -346,6 +346,16 @@ fn api_routes(rate_limit: u64) -> Router<AppState> {
|
||||
.route(
|
||||
"/watch-queue/dismiss",
|
||||
routing::post(handlers::webhook::post_dismiss_watch_events),
|
||||
)
|
||||
.route(
|
||||
"/wrapups/generate",
|
||||
routing::post(handlers::wrapup::post_generate),
|
||||
)
|
||||
.route("/wrapups", routing::get(handlers::wrapup::get_list))
|
||||
.route("/wrapups/{id}", routing::get(handlers::wrapup::get_status))
|
||||
.route(
|
||||
"/wrapups/{id}/report",
|
||||
routing::get(handlers::wrapup::get_report),
|
||||
);
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
|
||||
Reference in New Issue
Block a user