## 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
8.5 KiB
Backend — Familienarchiv
Overview
Spring Boot 4.0 monolith serving the Familienarchiv REST API. Handles document management, person/entity tracking, transcription workflows, OCR orchestration, user management, and full-text search.
Tech Stack
- Framework: Spring Boot 4.0 (Java 21)
- Build: Maven (
./mvnwwrapper) - Server: Jetty (not Tomcat — excluded in pom.xml)
- Data: PostgreSQL 16, JPA/Hibernate, Spring Data JPA
- Migrations: Flyway (SQL files in
src/main/resources/db/migration/) - Security: Spring Security, Spring Session JDBC
- File Storage: MinIO via AWS SDK v2 (S3-compatible)
- Spreadsheet Import: Apache POI 5.5.0 (Excel/ODS)
- API Docs: SpringDoc OpenAPI 3.x (
/v3/api-docs— dev profile only) - Monitoring: Spring Boot Actuator (
/actuator/health)
Package Structure
src/main/java/org/raddatz/familienarchiv/
├── audit/ # Audit logging (AuditService, AuditLogQueryService)
├── auth/ # AuthService, AuthSessionController, LoginRequest (Spring Session JDBC — ADR-020)
├── config/ # Infrastructure config (MinioConfig, AsyncConfig, WebConfig)
├── dashboard/ # Dashboard analytics + StatsController/StatsService
├── document/ # Document domain — entities, controller, service, repository, DTOs
│ ├── annotation/ # DocumentAnnotation, AnnotationService, AnnotationController
│ ├── comment/ # DocumentComment, CommentService, CommentController
│ └── transcription/ # TranscriptionBlock, TranscriptionService, TranscriptionBlockQueryService
├── exception/ # DomainException, ErrorCode, GlobalExceptionHandler
├── filestorage/ # FileService (S3/MinIO)
├── geschichte/ # Geschichte (story) domain — GeschichteService, GeschichteQueryService
│ └── journeyitem/ # JourneyItem sub-domain — JourneyItemService, JourneyItemController
├── importing/ # CanonicalImportOrchestrator + 4 loaders + CanonicalSheetReader
├── notification/ # Notification domain + SseEmitterRegistry
├── ocr/ # OCR domain — OcrService, OcrBatchService, training
├── person/ # Person domain — Person, PersonService, PersonController
│ └── relationship/ # PersonRelationship sub-domain
├── security/ # SecurityConfig, Permission, @RequirePermission, PermissionAspect
├── tag/ # Tag domain — Tag, TagService, TagController
└── user/ # User domain — AppUser, UserGroup, UserService
For per-domain ownership and public surface, see each domain's README.md.
Layering Rules
→ See docs/ARCHITECTURE.md §Layering rule
LLM reminder: controllers never call repositories directly; services never reach into another domain's repository — always call the other domain's service.
Key Entities
| Entity | Table | Key Relationships |
|---|---|---|
Document |
documents |
ManyToOne sender (Person), ManyToMany receivers (Person), ManyToMany tags (Tag) |
Person |
persons |
Referenced by documents as sender/receiver; name aliases table |
Tag |
tag |
ManyToMany with documents via document_tags; self-referencing parent for tree |
AppUser |
app_users |
ManyToMany groups (UserGroup) |
UserGroup |
user_groups |
Has a Set<String> permissions |
TranscriptionBlock |
transcription_blocks |
Per-document, per-page text blocks with polygons |
DocumentAnnotation |
document_annotations |
Free-form annotations on document pages |
Comment |
document_comments |
Threaded comments with mentions |
Notification |
notifications |
User notification feed |
OcrJob / OcrJobDocument |
ocr_jobs, ocr_job_documents |
Batch OCR job tracking |
DocumentStatus lifecycle: PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED
Entity Code Style
All entities use these Lombok annotations:
@Entity
@Table(name = "table_name")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MyEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
private UUID id;
// ...
}
@Schema(requiredMode = REQUIRED)on every field the backend always populates — drives TypeScript generation.- Collections use
@Builder.Defaultwithnew HashSet<>()as default. - Timestamps use
@CreationTimestamp/@UpdateTimestamp.
Services
- Annotated with
@Service,@RequiredArgsConstructor, optionally@Slf4j. - Write methods:
@Transactional. - Read methods: no annotation (default non-transactional) — except when the method returns
an entity whose lazy associations must remain accessible to the caller after the method
returns. In that case, use
@Transactional(readOnly = true)to keep the Hibernate session open. Removing this annotation causesLazyInitializationExceptionin production. See ADR-022. - Cross-domain access goes through the other domain's service, never its repository.
Error Handling
→ See CONTRIBUTING.md §Error handling
LLM reminder: use DomainException.notFound/forbidden/conflict/internal() — never throw raw exceptions from service methods. For simple controller validation (not domain logic), ResponseStatusException is acceptable: throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "…"). When adding a new ErrorCode: add to ErrorCode.java, mirror in frontend/src/lib/shared/errors.ts, add i18n keys in messages/{de,en,es}.json.
Security / Permissions
→ See docs/ARCHITECTURE.md §Permission system
LLM reminder: @RequirePermission(Permission.WRITE_ALL) is required on every POST, PUT, PATCH, DELETE endpoint — not optional. Do not mix with Spring Security's @PreAuthorize. Available permissions: READ_ALL, WRITE_ALL, ADMIN, ADMIN_USER, ADMIN_TAG, ADMIN_PERMISSION, ANNOTATE_ALL, BLOG_WRITE.
OCR Integration
The backend orchestrates OCR by calling the Python ocr-service microservice via RestClient:
OcrClientinterface — mockable for testsRestClientOcrClient— implementation using SpringRestClientOcrService— orchestrates presigned URL generation, OCR call, block mappingOcrBatchService— handles batch/job workflowsOcrAsyncRunner— async execution of OCR jobs
For ocr-service internals, see ocr-service/README.md.
API Testing
HTTP test files in backend/api_tests/ for the VS Code REST Client extension.
How to Run
cd backend
./mvnw spring-boot:run # Run with dev profile (requires PostgreSQL + MinIO)
./mvnw clean package # Build JAR (with tests)
./mvnw clean package -DskipTests
./mvnw test # Run all tests
./mvnw test -Dtest=ClassName # Run a single test class
./mvnw clean verify # Run with JaCoCo coverage report
OpenAPI / TypeScript type generation:
- Start backend with
--spring.profiles.active=dev - In
frontend/:npm run generate:api
LLM reminder: always regenerate types after any model or endpoint change — the most common cause of "where did my TypeScript type go?"
Testing
- Unit tests: Mockito + JUnit, pure in-memory
- Slice tests:
@WebMvcTest,@DataJpaTestwith Testcontainers PostgreSQL - Integration tests: Full Spring context with Testcontainers
- Coverage gate: 88% branch coverage (JaCoCo)