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:
|
Hexagonal (Ports & Adapters) with Domain-Driven Design:
|
||||||
|
|
||||||
```
|
```
|
||||||
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui
|
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas) + HtmlPageContext; used by presentation, tui, and template adapters
|
||||||
domain — pure types and trait definitions, no external deps
|
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
|
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
|
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)
|
worker — standalone worker binary (event consumer, poster sync, federation)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ graph TB
|
|||||||
M_SEARCH["SearchQuery,<br/>SearchResults"]
|
M_SEARCH["SearchQuery,<br/>SearchResults"]
|
||||||
end
|
end
|
||||||
subgraph Ports["Port Traits (Interfaces)"]
|
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_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/>ObjectStorage<br/>EventPublisher<br/>EventConsumer<br/>PasswordHasher<br/>DiaryExporter<br/>DocumentParser"]
|
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_SEARCH["SearchPort<br/>SearchCommand<br/>PersonQuery<br/>PersonCommand"]
|
||||||
P_FEDERATION["SocialQueryPort<br/>LocalApContentQuery<br/>RemoteWatchlistRepository<br/>RemoteGoalRepository"]
|
P_FEDERATION["SocialQueryPort<br/>LocalApContentQuery<br/>RemoteWatchlistRepository<br/>RemoteGoalRepository"]
|
||||||
end
|
end
|
||||||
@@ -72,8 +72,12 @@ graph TB
|
|||||||
VO["Value Objects<br/><i>MovieId, UserId, Rating,<br/>WatchMedium, Email, Username,<br/>Password, ...</i>"]
|
VO["Value Objects<br/><i>MovieId, UserId, Rating,<br/>WatchMedium, Email, Username,<br/>Password, ...</i>"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph ApiTypes["api-types (0 domain deps)"]
|
subgraph ApiTypes["api-types"]
|
||||||
DTO["DTOs<br/><i>MovieDto, ReviewDto,<br/>FeedEntryDto, UserSummaryDto,<br/>CastMemberDto, ...</i>"]
|
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
|
end
|
||||||
|
|
||||||
subgraph Adapters["Adapters (implement Port Traits)"]
|
subgraph Adapters["Adapters (implement Port Traits)"]
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ impl From<DomainError> for ApiError {
|
|||||||
|
|
||||||
impl IntoResponse for ApiError {
|
impl IntoResponse for ApiError {
|
||||||
fn into_response(self) -> Response {
|
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,
|
"settingRebuildPath": true,
|
||||||
"settingFollowRedirects": "global"
|
"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",
|
"_id": "fld_people",
|
||||||
|
|||||||
Reference in New Issue
Block a user