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

6.7 KiB
Raw Blame History

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.