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:
@@ -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)
|
||||
|
||||
@@ -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)"]
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user