Files
werkbaum/backend/README.md
T
mhoennigandClaude Opus 5 d5cdff6058 feat(backend): Historie in zwei Ebenen, gezielter Repository-Zugriff (Schritt 2)
Meilensteine sind die nutzersichtbare Historie und bleiben; Sync-Versionen
tragen die Diffs des Live-Editings und werden nach der Aufbewahrungsfrist
verdichtet (D76). Ohne die Trennung wuerde die Historie beim getakteten
Schreiben zum Transaktionslog.

Die Schreibpause braucht keinen Zeitgeber: Die naechste Aenderung stellt
fest, dass eine Pause war, und befoerdert die Version davor nachtraeglich.
Strukturelle Aenderungen sind immer Meilensteine.

Das Historie-Repository greift jetzt gezielt zu (eine Version, juengster,
aeltester, Meilensteine, maxVersion) statt stets alle Eintraege zu laden und
in Kotlin zu filtern — bei hunderten Volltext-Versionen je Dokument war das
untragbar. Restore liest den letzten Stand aus dem Tombstone: der ueberlebt
das Verdichten, die Version davor womoeglich nicht.

Dabei die D76-Unschaerfe aufgeloest: RESTORED heisst nur noch "ein
geloeschtes Dokument ist wieder da" (der Client hebt seine Sperre auf), der
Rueckfall eines lebenden Dokuments ist ROLLED_BACK.

81 Tests. Gegenprobe: Schreibpause ignoriert -> genau die danach benannte
Zusicherung faellt; Rueckfall wieder als RESTORED -> Unit- und
Cucumber-Test dazu; juengster Stand aus der Historie genommen -> genau einer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:51:46 +02:00

133 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Editor Backend Grundgerippe
CRUD-Skelett mit **Spring Boot 4**, **Kotlin** und **API First** (OpenAPI 3, YAML).
Die vier HTTP-Befehle (GET, POST, PUT, DELETE) sind für die Ressource `Document`
umgesetzt bewusst noch **ohne Autorisierung**, aber mit vorbereiteten
Erweiterungspunkten für Live-Editing, Autorisierung und clientseitige
Verschlüsselung.
## Voraussetzungen
- JDK 21
- Gradle 9 (einmalig `gradle wrapper --gradle-version 9.1` ausführen, danach `./gradlew`)
## Wichtige Kommandos
| Kommando | Zweck |
|------------------------------|------------------------------------------------------------------------------|
| `./gradlew openApiGenerate` | Generiert API-Interfaces + Modelle aus `src/main/resources/openapi/api.yaml` |
| `./gradlew build` | Generierung, Kompilierung, alle Tests, Coverage-Prüfung |
| `./gradlew test` | Unit- und Behavior-Tests (Cucumber) |
| `./gradlew jacocoTestReport` | Coverage-Report unter `build/reports/jacoco/test/html` |
| `./gradlew bootRun` | Startet das Backend auf Port 8080 |
## API First Ablauf
1. Vertrag ändern: `src/main/resources/openapi/api.yaml`
2. `./gradlew openApiGenerate` → erzeugt `DocumentsApi` (Interface) und Modelle
nach `build/generated/openapi` (Pakete `de.werkbaum.generated.*`)
3. `DocumentsController` implementiert das Interface mit
`skipDefaultInterface=true`: Weicht die Implementierung vom Vertrag ab,
**bricht der Build** Spezifikation und Code können nicht auseinanderlaufen.
Generierter Code wird nicht eingecheckt und zählt nicht zur Code Coverage.
## Teststrategie
- **Behavior-Tests (Cucumber, `src/test/resources/features/dokumente.feature`)**
testen die API von außen gegen die laufende Anwendung (`RANDOM_PORT`):
Statuscodes, Payloads, Fehlerpfade. Die Szenarien sind auf Deutsch
(`# language: de`) und dienen als lebende Dokumentation.
- **Unit-Tests (JUnit 5 + MockK)** decken die Geschäftslogik im
`DocumentService` isoliert ab (Versionierung, Zeitstempel per festem `Clock`,
Fehlerfälle).
- **Coverage**: JaCoCo, Verifikation mit mind. 80 % Line Coverage
(`jacocoTestCoverageVerification`, hängt an `check`).
## Architektur
```
api/ DocumentsController (implementiert generiertes Interface),
GlobalExceptionHandler (RFC 9457 ProblemDetail)
service/ DocumentService (Geschäftslogik, Versionierung), Clock-Bean
repository/ DocumentRepository + DocumentHistoryRepository (Interfaces)
persistence/ JPA-Entities, Spring-Data-Repositories und Adapter (H2/Liquibase)
domain/ Document (internes Modell, getrennt vom API-Modell)
```
Das interne Domänenmodell ist bewusst vom generierten API-Modell getrennt
so können API-Vertrag und Persistenz unabhängig voneinander weiterentwickelt
werden.
## Historie & Wiederherstellung
- Jede Änderung wird als Snapshot in einer vom Dokument getrennten Historie
protokolliert sie **überlebt ein DELETE**.
- **Zwei Ebenen** (D76): **Meilensteine** sind die nutzersichtbare Historie und
bleiben; **Sync-Versionen** tragen die Diffs des Live-Editings, sind
kurzlebig und werden verdichtet. Ohne die Trennung würde die Historie beim
getakteten Schreiben zum Transaktionslog hunderte Volltext-Snapshots eines
40-kB-Dokuments je Sitzung.
- Meilenstein wird ein Stand bei einer strukturellen Änderung (Anlegen,
Löschen, Wiederherstellen, Rückfall), beim Vollersatz per `PUT` und
**nach einer Schreibpause**. Letzteres ohne Zeitgeber: Die nächste
Änderung stellt fest, dass eine Pause war, und befördert die Version
davor nachträglich. Der Knopfdruck aus dem Konzept ist derselbe
Schalter (`milestone = true`), sobald `PATCH /content` ihn durchreicht.
- Stellschrauben: `werkbaum.live-editing.milestone-pause` (30 s) und
`sync-retention` (1 h).
- `GET /api/v1/documents/{uuid}/history` liefert die Meilensteine (älteste
zuerst) und immer den jüngsten Stand; Identifier ist die UUID, wie bei GET
(der Titel ist nicht eindeutig).
- `POST /api/v1/documents/{uuid}/restore` stellt ein gelöschtes Dokument unter
derselben UUID wieder her (`RESTORED`, letzter Stand aus dem Tombstone). Mit
optionalem Body `{"version": n}` wird eine bestimmte Version übernommen bei
einem noch lebenden Dokument ist das ein Rückfall (`ROLLED_BACK`), kein
Wiederherstellen: Der Client hatte nie eine Sperre. Ohne Zielversion
antwortet der Server bei existierendem Dokument mit 409, eine bereits
verdichtete Zielversion mit 404.
## Vorbereitete Erweiterungen
**Autorisierung**
- `bearerAuth` (JWT) ist in der OpenAPI-Spec als Security Scheme definiert,
aber noch auf keine Operation angewendet.
- Später: `spring-boot-starter-security` + `security: [bearerAuth]` in der
Spec; die Behavior-Tests erhalten dann einen Auth-Schritt
(„Angenommen ich bin als … angemeldet").
**Live-Editing** (Konzept: `docs/live-editing-proposal.md`, Entscheidung: D76)
- **Gebaut:** das Zeilen-Diff als reine Funktionen (`de.werkbaum.diff`
Anwenden, Berechnen, Rebasen, Prüfsumme) und die zweistufige Historie.
- **Offen:** `PATCH /content` samt Rebase und Idempotenz, der Änderungsfeed per
Long Polling, Master-Passwort für `GET /documents`, Client-Anpassung.
- `DocumentUpdateRequest.expectedVersion` ist im Vertrag vorgesehen, wird aber
noch nicht ausgewertet.
**Clientseitige Verschlüsselung**
- `content` ist ein opaker String, den der Server nie interpretiert. Der
Wechsel auf Ciphertext erfordert keine API-Änderung; ggf. kommen später
Metadaten-Felder (z. B. Schlüssel-ID, Nonce) als eigene Properties hinzu.
## Persistenz
- **H2 im File-Modus** (`./data/editor.mv.db`) mit `MODE=PostgreSQL`
läuft im Server-Prozess, keine Datenbank-Installation nötig. Dokumente und
Historie überleben einen Neustart.
- **Schema per Liquibase im formatierten SQL-Format**
(`src/main/resources/db/changelog/db.changelog-master.sql`, kein XML).
Neue Änderungen werden als weitere `--changeset`-Blöcke angehängt;
Hibernate validiert nur (`ddl-auto: validate`).
- **Umstieg auf echtes PostgreSQL:** im Wesentlichen JDBC-URL/Credentials in
der `application.yaml` tauschen und den Postgres-Treiber als Dependency
ergänzen Schema-Migrationen und Code bleiben unverändert.
- Tests laufen gegen H2 in-memory (`src/test/resources/application.yaml`),
mit demselben Liquibase-Schema.
- Service-Methoden sind `@Transactional`: Dokument-Änderung und
Historieneintrag werden atomar geschrieben.
## Hinweise
- Versionsnummern in `build.gradle.kts` (Spring Boot, OpenAPI Generator,
Cucumber, MockK) beim ersten Build ggf. auf den aktuellen Patch-Stand heben.