docs+fix: update arch diagram/README/Insomnia, ApiError returns JSON

- architecture.mmd: CQRS trait names, ImageFetcher/RssFeedRenderer
  ports, infra-wiring crate, api-types description
- README: add infra-wiring, note CQRS ports
- Insomnia: add PATCH /api/v1/reviews/:id
- ApiError returns {"error":"..."} JSON not plain text
This commit is contained in:
2026-07-10 03:52:49 +02:00
parent dee013c7eb
commit 2e32847129
4 changed files with 45 additions and 7 deletions

View File

@@ -88,8 +88,9 @@ Open `http://localhost:3000`. The HTTP server and background worker start togeth
Hexagonal (Ports & Adapters) with Domain-Driven Design:
```
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui
domain — pure types and trait definitions, no external deps
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas) + HtmlPageContext; used by presentation, tui, and template adapters
infra-wiring — shared infrastructure types (DbPool, EventBusBackend, AppConfig) used by both presentation and worker binaries
domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, ImageFetcher, RssFeedRenderer), no external deps except serde
application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic
presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving, composition root for the HTTP process
worker — standalone worker binary (event consumer, poster sync, federation)

View File

@@ -59,8 +59,8 @@ graph TB
M_SEARCH["SearchQuery,<br/>SearchResults"]
end
subgraph Ports["Port Traits (Interfaces)"]
P_REPOS["MovieRepository<br/>ReviewRepository<br/>DiaryRepository<br/>UserRepository<br/>WatchlistRepository<br/>WatchEventRepository<br/>WebhookTokenRepository<br/>ImportSessionRepository<br/>MovieProfileRepository<br/>WrapUpRepository<br/>GoalRepository<br/>UserSettingsRepository<br/>MovieDeduplicator"]
P_SERVICES["AuthService<br/>MetadataClient<br/>PosterFetcherClient<br/>ObjectStorage<br/>EventPublisher<br/>EventConsumer<br/>PasswordHasher<br/>DiaryExporter<br/>DocumentParser"]
P_REPOS["MovieCommand / MovieQuery<br/>ReviewRepository<br/>DiaryRepository / StatsRepository<br/>UserRepository<br/>WatchlistRepository<br/>WatchEventCommand / WatchEventQuery<br/>WebhookTokenRepository<br/>ImportSessionRepository<br/>MovieProfileRepository<br/>WrapUpRepository<br/>GoalRepository<br/>UserSettingsRepository<br/>MovieDeduplicator"]
P_SERVICES["AuthService<br/>MetadataClient<br/>PosterFetcherClient<br/>ImageFetcher<br/>ObjectStorage<br/>EventPublisher<br/>EventConsumer<br/>PasswordHasher<br/>DiaryExporter<br/>DocumentParser<br/>RssFeedRenderer"]
P_SEARCH["SearchPort<br/>SearchCommand<br/>PersonQuery<br/>PersonCommand"]
P_FEDERATION["SocialQueryPort<br/>LocalApContentQuery<br/>RemoteWatchlistRepository<br/>RemoteGoalRepository"]
end
@@ -72,8 +72,12 @@ graph TB
VO["Value Objects<br/><i>MovieId, UserId, Rating,<br/>WatchMedium, Email, Username,<br/>Password, ...</i>"]
end
subgraph ApiTypes["api-types (0 domain deps)"]
DTO["DTOs<br/><i>MovieDto, ReviewDto,<br/>FeedEntryDto, UserSummaryDto,<br/>CastMemberDto, ...</i>"]
subgraph ApiTypes["api-types"]
DTO["DTOs<br/><i>MovieDto, ReviewDto,<br/>FeedEntryDto, UserSummaryDto,<br/>HtmlPageContext, ...</i>"]
end
subgraph InfraWiring["infra-wiring"]
IW["DbPool, EventBusBackend,<br/>AppConfig<br/><i>Shared infra types</i>"]
end
subgraph Adapters["Adapters (implement Port Traits)"]

View File

@@ -36,6 +36,13 @@ impl From<DomainError> for ApiError {
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
domain_error_response(self.0)
let status = domain_error_status(&self.0);
match &self.0 {
DomainError::InfrastructureError(_) => {
tracing::error!("Internal error: {:?}", self.0);
(status, axum::Json(serde_json::json!({"error": "internal server error"}))).into_response()
}
_ => (status, axum::Json(serde_json::json!({"error": self.0.to_string()}))).into_response(),
}
}
}

View File

@@ -355,6 +355,32 @@
"settingRebuildPath": true,
"settingFollowRedirects": "global"
},
{
"_id": "req_reviews_edit",
"_type": "request",
"parentId": "fld_reviews",
"modified": 1749340800000,
"created": 1749340800000,
"name": "Edit Review",
"method": "PATCH",
"url": "{{ base_url }}/api/v1/reviews/:id",
"description": "Partial update — all fields optional",
"headers": [{ "name": "Content-Type", "value": "application/json" }],
"parameters": [],
"authentication": { "type": "bearer", "token": "{{ token }}" },
"body": {
"mimeType": "application/json",
"text": "{\n \"rating\": 9,\n \"comment\": \"Updated thoughts\",\n \"watch_medium\": \"cinema\"\n}"
},
"metaSortKey": -3,
"isPrivate": false,
"settingStoreCookies": true,
"settingSendCookies": true,
"settingDisableRenderRequestBody": false,
"settingEncodeUrl": true,
"settingRebuildPath": true,
"settingFollowRedirects": "global"
},
{
"_id": "fld_people",