feat(geschichte): JourneyItem CRUD API — append, updateNote, delete, reorder (#751) (#788)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
## Summary Implements the backend JourneyItem CRUD API on top of the data model from #750, building towards the full Lesereisen feature (#751). **Completed in this PR:** - `jackson-databind-nullable` 0.2.6 + `JacksonConfig` (`@Bean Module`) for three-way PATCH semantics (`JsonNullable`) - `AuditKind`: `JOURNEY_ITEM_ADDED`, `JOURNEY_ITEM_REMOVED`, `JOURNEY_ITEMS_REORDERED` (last is rollup-eligible) - `ErrorCode`: `JOURNEY_ITEM_NOT_FOUND`, `JOURNEY_ITEM_POSITION_CONFLICT` - V73 migration: `UNIQUE (geschichte_id, position) DEFERRABLE INITIALLY DEFERRED` + `CHECK (position > 0)` on `journey_items` - `JourneyItemConstraintsTest`: verifies deferrable flag via `pg_constraint` query; position check; duplicate position rejection (3 passing tests) - Read models: `DocumentSummary`, `JourneyItemView`, `GeschichteView` (with `AuthorView` to prevent AppUser email leak) - `DocumentService.getSummaryById` — lean lookup without tag-color resolution - `JourneyItemRepository`: extended with `findByGeschichteIdOrderByPosition`, `findByIdAndGeschichteId` (IDOR-safe), `findIdsByGeschichteId`, `findMaxPositionByGeschichteId`, `countByGeschichteId` - DTOs: `JourneyItemCreateDTO`, `JourneyItemUpdateDTO` (`JsonNullable<String> note`), `JourneyReorderDTO` **Still in progress (WIP):** - `JourneyItemService` — `append`, `updateNote`, `delete`, `reorder`, `toSummary`, `toView` (Task 6) - `GeschichteService.getById` → returns `GeschichteView` (Task 7) - New endpoints on `GeschichteController` + slice tests (Task 8) - Frontend error codes + i18n + `npm run generate:api` (Task 9) ## Commits - `0b177247` feat(config): add jackson-databind-nullable for JsonNullable PATCH DTO support - `408ae334` feat(audit,error): add JourneyItem AuditKind values and ErrorCodes - `7b06c3ad` feat(migration): V73 adds UNIQUE DEFERRABLE and CHECK position > 0 on journey_items - `160ca1c3` feat(geschichte): add DocumentSummary, JourneyItemView, GeschichteView read models - `2ad5c36e` feat(geschichte): extend JourneyItemRepository and add item DTOs ## Test plan - [ ] `./mvnw test -Dtest=JourneyItemConstraintsTest` — all 3 constraint tests pass - [ ] `./mvnw clean package -DskipTests` — builds clean after remaining tasks are merged - [ ] Frontend: `npm run generate:api` after Task 9 endpoint additions Co-authored-by: Marcel <marcel@familienarchiv> Reviewed-on: #788
This commit was merged in pull request #788.
This commit is contained in:
@@ -18,6 +18,8 @@ System_Boundary(backend, "API Backend (Spring Boot)") {
|
||||
Component(sseRegistry, "SseEmitterRegistry", "Spring Component", "In-memory ConcurrentHashMap of Spring SseEmitter instances per user. Handles registration, deregistration, and JSON event broadcasts.")
|
||||
Component(geschCtrl, "GeschichteController", "Spring MVC — /api/geschichten", "CRUD for publishable stories (STORY) and reading journeys (JOURNEY). Returns GeschichteSummary projections for list; full Geschichte with JourneyItems for detail. Requires BLOG_WRITE permission for write operations.")
|
||||
Component(geschSvc, "GeschichteService", "Spring Service", "Manages story lifecycle (DRAFT → PUBLISHED with timestamp). Supports two subtypes: STORY (prose) and JOURNEY (ordered JourneyItem sequence). Sanitizes HTML body with an allowlist policy.")
|
||||
Component(geschQuerySvc, "GeschichteQueryService", "Spring Service", "Read-only facade over GeschichteRepository. Exposes existsById() and findById() to prevent JourneyItemService from crossing domain boundaries.")
|
||||
Component(journeyItemSvc, "JourneyItemService", "Spring Service", "Manages journey item lifecycle: append (100-item cap), updateNote (three-way PATCH), delete, and reorder (DEFERRABLE position swap). Enforces JOURNEY-type guard on append.")
|
||||
Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.")
|
||||
}
|
||||
|
||||
@@ -38,6 +40,10 @@ Rel(notifCtrl, notifSvc, "Delegates to")
|
||||
Rel(notifCtrl, sseRegistry, "Registers client SSE connection")
|
||||
Rel(notifSvc, sseRegistry, "Broadcasts events to connected clients")
|
||||
Rel(geschCtrl, geschSvc, "Delegates to")
|
||||
Rel(geschCtrl, journeyItemSvc, "Delegates journey item CRUD")
|
||||
Rel(journeyItemSvc, geschQuerySvc, "Checks Geschichte existence and type")
|
||||
Rel(geschQuerySvc, db, "Reads geschichten", "JDBC")
|
||||
Rel(journeyItemSvc, db, "Reads / writes journey_items", "JDBC")
|
||||
Rel(auditSvc, db, "Writes audit_log", "JDBC")
|
||||
Rel(auditQuery, db, "Reads audit_log", "JDBC")
|
||||
Rel(notifSvc, db, "Reads / writes notifications", "JDBC")
|
||||
|
||||
Reference in New Issue
Block a user