From 6fc5ce6ddd6e36c231c038f9dafad4e42242eae4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 8 Jun 2026 17:06:25 +0200 Subject: [PATCH] docs: update GLOSSARY for JourneyItem view types; add ADR-035 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes GLOSSARY position-step value (1000→10), adds DEFERRABLE constraint note, and documents GeschichteView, JourneyItemView, and DocumentSummary read-model types. ADR-035 records the decision to use Optional for three-way PATCH semantics instead of jackson-databind-nullable (which targets Jackson 2.x and is incompatible with Spring Boot 4.0 / Jackson 3.x). Co-Authored-By: Claude Sonnet 4.6 --- docs/GLOSSARY.md | 8 +++- ...tional-string-three-way-patch-semantics.md | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 docs/adr/035-optional-string-three-way-patch-semantics.md diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 7dd3419a..e38eb070 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -151,7 +151,13 @@ _See also [Chronik](#chronik-internal)._ **Geschichte** (`Geschichte`) `[user-facing]` — a narrative story or curated document journey published in the archive. Two subtypes: `STORY` (free-form prose linking `Person`s) and `JOURNEY` (a *Lesereise* — an ordered sequence of `JourneyItem`s). Lifecycle: `DRAFT → PUBLISHED` (see `GeschichteStatus`). DRAFT stories are hidden from users without the `BLOG_WRITE` permission. -**JourneyItem** (`JourneyItem`, table `journey_items`) `[internal]` — a single stop in a *Lesereise* (`Geschichte` with `type=JOURNEY`). Either document-backed (`document_id IS NOT NULL`) or a note-only interlude (`note IS NOT NULL`). Ordered by `position` (gaps of 1000 leave room for drag-reorder). A CHECK constraint ensures at least one of `document_id` or `note` is present. The FK to `documents` uses `ON DELETE SET NULL`, so deleting a document preserves the item (with `document_id = null`). +**JourneyItem** (`JourneyItem`, table `journey_items`) `[internal]` — a single stop in a *Lesereise* (`Geschichte` with `type=JOURNEY`). Either document-backed (`document_id IS NOT NULL`) or a note-only interlude (`note IS NOT NULL`). Ordered by `position` (step of 10; max 100 items per journey). A DEFERRABLE UNIQUE constraint on `(geschichte_id, position)` allows atomic position swaps in the same transaction. A CHECK constraint ensures at least one of `document_id` or `note` is present. The FK to `documents` uses `ON DELETE SET NULL`, so deleting a document preserves the item (with `document_id = null`). + +**GeschichteView** (`GeschichteView`) `[internal]` — lean read-model record returned by `GeschichteService.getById()`. Contains `AuthorView` (id + displayName only — email not exposed) and a `List` loaded via a separate query rather than a lazy collection. + +**JourneyItemView** (`JourneyItemView`) `[internal]` — lean view record for a single `JourneyItem` surface, containing `id`, `position`, an optional `DocumentSummary`, and an optional `note`. + +**DocumentSummary** (`DocumentSummary`) `[internal]` — lean document read-model used inside `JourneyItemView`. Contains title, date, senderName, receiverName, receiverCount, datePrecision — no tags or file storage info. **Lesereise** `[user-facing]` — a curated reading journey through a sequence of family documents, optionally annotated with editorial notes. Implemented as a `Geschichte` with `type=JOURNEY`. The reader UI (follow-on issue) renders items as a sequential reading experience. diff --git a/docs/adr/035-optional-string-three-way-patch-semantics.md b/docs/adr/035-optional-string-three-way-patch-semantics.md new file mode 100644 index 00000000..29979bff --- /dev/null +++ b/docs/adr/035-optional-string-three-way-patch-semantics.md @@ -0,0 +1,43 @@ +# ADR-035 — `Optional` for three-way PATCH semantics + +**Status:** Accepted +**Date:** 2026-06-08 +**Issue:** #751 (JourneyItem CRUD API) + +## Context + +The `PATCH /api/geschichten/{id}/items/{itemId}` endpoint must distinguish three cases for the `note` field: + +| JSON body | Intended meaning | +|-------------------|-----------------------| +| `{"note": "text"}`| Set note to "text" | +| `{"note": null}` | Clear the note | +| `{}` (absent) | Leave note unchanged | + +The standard library for this on Jackson 2.x is `jackson-databind-nullable` (`JsonNullable` from `org.openapitools`). However, that library targets `com.fasterxml.jackson.*` (Jackson 2.x) and is incompatible with Spring Boot 4.0 / Spring Framework 7, which uses `tools.jackson.*` (Jackson 3.x). The module fails to register and throws at startup. + +## Decision + +Use `Optional` with Java's default field initializer (`= null`) to encode the three states: + +```java +@Data +public class JourneyItemUpdateDTO { + private Optional note = null; // Java default — absent = no-op +} +``` + +| Java value | JSON wire | Semantics | +|--------------------|-------------------|---------------| +| `null` (default) | field absent | no-op | +| `Optional.empty()` | `{"note": null}` | clear | +| `Optional.of("x")` | `{"note": "x"}` | set | + +Jackson 3.x natively maps a JSON `null` to `Optional.empty()` and leaves absent fields at their Java default. No custom module is needed. + +## Consequences + +- No external dependency for PATCH semantics — simpler pom.xml. +- The DTO field type is `Optional`, not `String` — service code must null-check the field first (`if (noteField == null) return;`) and then call `.orElse(null)` to unwrap. +- This pattern applies to any future PATCH DTO that needs three-way semantics on a nullable field. +- `jackson-databind-nullable` is removed from `pom.xml`; `JacksonConfig.java` is kept as a placeholder for future custom modules.