feat: wrap-up REST API endpoints
This commit is contained in:
@@ -8,6 +8,7 @@ pub mod social;
|
|||||||
pub mod users;
|
pub mod users;
|
||||||
pub mod watchlist;
|
pub mod watchlist;
|
||||||
pub mod webhook;
|
pub mod webhook;
|
||||||
|
pub mod wrapup;
|
||||||
|
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
pub use common::*;
|
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 import;
|
||||||
pub mod rss;
|
pub mod rss;
|
||||||
pub mod webhook;
|
pub mod webhook;
|
||||||
|
pub mod wrapup;
|
||||||
|
|
||||||
const DEFAULT_PAGE_LIMIT: u32 = 5;
|
const DEFAULT_PAGE_LIMIT: u32 = 5;
|
||||||
const RSS_FEED_LIMIT: u32 = 50;
|
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 users;
|
||||||
mod watchlist;
|
mod watchlist;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
mod wrapup;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
@@ -42,6 +43,7 @@ fn build() -> utoipa::openapi::OpenApi {
|
|||||||
api.merge(search::SearchDoc::openapi());
|
api.merge(search::SearchDoc::openapi());
|
||||||
api.merge(watchlist::WatchlistDoc::openapi());
|
api.merge(watchlist::WatchlistDoc::openapi());
|
||||||
api.merge(webhook::WebhookDoc::openapi());
|
api.merge(webhook::WebhookDoc::openapi());
|
||||||
|
api.merge(wrapup::WrapUpDoc::openapi());
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
api.merge(social::SocialDoc::openapi());
|
api.merge(social::SocialDoc::openapi());
|
||||||
SecurityAddon.modify(&mut api);
|
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(
|
.route(
|
||||||
"/watch-queue/dismiss",
|
"/watch-queue/dismiss",
|
||||||
routing::post(handlers::webhook::post_dismiss_watch_events),
|
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")]
|
#[cfg(feature = "federation")]
|
||||||
|
|||||||
Reference in New Issue
Block a user