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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0f3bd8f5b8
commit
d5cdff6058
@@ -1,9 +1,11 @@
|
||||
package de.werkbaum
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
class EditorBackendApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -3,12 +3,33 @@ package de.werkbaum.domain
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
enum class ChangeType { CREATED, UPDATED, DELETED, RESTORED }
|
||||
/**
|
||||
* Art der Änderung.
|
||||
*
|
||||
* [RESTORED] heißt ausschließlich: ein **gelöschtes** Dokument ist wieder da —
|
||||
* der Client hebt daraufhin seine Sperre auf. [ROLLED_BACK] ist der Rückfall
|
||||
* eines **lebenden** Dokuments auf eine alte Version; für den Client ein
|
||||
* gewöhnlicher Inhaltswechsel. Beide trugen früher denselben Typ; ein Typ, der
|
||||
* zwei Dinge bedeutet, ist die Unschärfe, aus der später Fehler werden (D76).
|
||||
*/
|
||||
enum class ChangeType { CREATED, UPDATED, DELETED, RESTORED, ROLLED_BACK }
|
||||
|
||||
/**
|
||||
* Ein Eintrag der Dokumenthistorie. Die Historie wird getrennt vom Dokument
|
||||
* gespeichert und überlebt daher ein DELETE – Grundlage für die
|
||||
* Wiederherstellung und später auch für Audit/Live-Editing-Replays.
|
||||
* Wiederherstellung und für die Diffs des Live-Editings.
|
||||
*
|
||||
* Die Historie hat **zwei Ebenen** (D76):
|
||||
* - [milestone] `false` — eine **Sync-Version**. Sie trägt das Protokoll
|
||||
* (Diffs zwischen beliebigen Versionen), ist kurzlebig und wird nach einer
|
||||
* Weile verdichtet. Danach beantwortet der Feed betroffene `since`-Werte
|
||||
* mit Volltext.
|
||||
* - [milestone] `true` — ein **Meilenstein**, die nutzersichtbare Historie.
|
||||
* Meilensteine entstehen bei strukturellen Änderungen, nach einer
|
||||
* Schreibpause und auf Knopfdruck; sie werden nie verdichtet.
|
||||
*
|
||||
* Ohne die Trennung würde die Historie bei 1,5 s Debounce zum Transaktionslog:
|
||||
* hunderte Volltext-Snapshots eines 40-kB-Dokuments je Sitzung.
|
||||
*/
|
||||
data class DocumentHistoryEntry(
|
||||
val documentId: UUID,
|
||||
@@ -17,4 +38,5 @@ data class DocumentHistoryEntry(
|
||||
val content: String,
|
||||
val changeType: ChangeType,
|
||||
val timestamp: OffsetDateTime,
|
||||
val milestone: Boolean = true,
|
||||
)
|
||||
|
||||
@@ -38,6 +38,10 @@ class DocumentHistoryEntity(
|
||||
|
||||
@Column(name = "change_time", nullable = false)
|
||||
val changeTime: OffsetDateTime,
|
||||
|
||||
/** Meilenstein (nutzersichtbar, bleibt) oder Sync-Version (wird verdichtet) – D76. */
|
||||
@Column(nullable = false)
|
||||
var milestone: Boolean = true,
|
||||
) {
|
||||
fun toDomain() = DocumentHistoryEntry(
|
||||
documentId = documentId,
|
||||
@@ -46,6 +50,7 @@ class DocumentHistoryEntity(
|
||||
content = content,
|
||||
changeType = changeType,
|
||||
timestamp = changeTime,
|
||||
milestone = milestone,
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -56,6 +61,7 @@ class DocumentHistoryEntity(
|
||||
content = entry.content,
|
||||
changeType = entry.changeType,
|
||||
changeTime = entry.timestamp,
|
||||
milestone = entry.milestone,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.werkbaum.persistence
|
||||
import de.werkbaum.domain.DocumentHistoryEntry
|
||||
import de.werkbaum.repository.DocumentHistoryRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
@@ -14,8 +15,28 @@ class JpaDocumentHistoryRepository(
|
||||
jpa.save(DocumentHistoryEntity.fromDomain(entry))
|
||||
}
|
||||
|
||||
override fun findByDocumentId(documentId: UUID): List<DocumentHistoryEntry> =
|
||||
jpa.findByDocumentIdOrderByIdAsc(documentId).map { it.toDomain() }
|
||||
override fun exists(documentId: UUID): Boolean = jpa.existsByDocumentId(documentId)
|
||||
|
||||
override fun findVersion(documentId: UUID, version: Long): DocumentHistoryEntry? =
|
||||
jpa.findFirstByDocumentIdAndVersionOrderByIdDesc(documentId, version)?.toDomain()
|
||||
|
||||
override fun findLatest(documentId: UUID): DocumentHistoryEntry? =
|
||||
jpa.findFirstByDocumentIdOrderByIdDesc(documentId)?.toDomain()
|
||||
|
||||
override fun findOldest(documentId: UUID): DocumentHistoryEntry? =
|
||||
jpa.findFirstByDocumentIdOrderByIdAsc(documentId)?.toDomain()
|
||||
|
||||
override fun maxVersion(documentId: UUID): Long? = jpa.maxVersion(documentId)
|
||||
|
||||
override fun findMilestones(documentId: UUID): List<DocumentHistoryEntry> =
|
||||
jpa.findByDocumentIdAndMilestoneTrueOrderByIdAsc(documentId).map { it.toDomain() }
|
||||
|
||||
override fun promoteToMilestone(documentId: UUID, version: Long) {
|
||||
jpa.promoteToMilestone(documentId, version)
|
||||
}
|
||||
|
||||
override fun compact(documentId: UUID, olderThan: OffsetDateTime): Int =
|
||||
jpa.deleteSyncVersionsOlderThan(documentId, olderThan)
|
||||
|
||||
override fun clear() = jpa.deleteAll()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,53 @@
|
||||
package de.werkbaum.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
interface DocumentJpaRepository : JpaRepository<DocumentEntity, UUID>
|
||||
|
||||
interface DocumentHistoryJpaRepository : JpaRepository<DocumentHistoryEntity, Long> {
|
||||
fun findByDocumentIdOrderByIdAsc(documentId: UUID): List<DocumentHistoryEntity>
|
||||
|
||||
fun existsByDocumentId(documentId: UUID): Boolean
|
||||
|
||||
fun findFirstByDocumentIdAndVersionOrderByIdDesc(
|
||||
documentId: UUID,
|
||||
version: Long,
|
||||
): DocumentHistoryEntity?
|
||||
|
||||
fun findFirstByDocumentIdOrderByIdDesc(documentId: UUID): DocumentHistoryEntity?
|
||||
|
||||
fun findFirstByDocumentIdOrderByIdAsc(documentId: UUID): DocumentHistoryEntity?
|
||||
|
||||
fun findByDocumentIdAndMilestoneTrueOrderByIdAsc(documentId: UUID): List<DocumentHistoryEntity>
|
||||
|
||||
@Query("select max(e.version) from DocumentHistoryEntity e where e.documentId = :documentId")
|
||||
fun maxVersion(@Param("documentId") documentId: UUID): Long?
|
||||
|
||||
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||
@Query(
|
||||
"""
|
||||
update DocumentHistoryEntity e set e.milestone = true
|
||||
where e.documentId = :documentId and e.version = :version
|
||||
"""
|
||||
)
|
||||
fun promoteToMilestone(
|
||||
@Param("documentId") documentId: UUID,
|
||||
@Param("version") version: Long,
|
||||
): Int
|
||||
|
||||
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||
@Query(
|
||||
"""
|
||||
delete from DocumentHistoryEntity e
|
||||
where e.documentId = :documentId and e.milestone = false and e.changeTime < :cutoff
|
||||
"""
|
||||
)
|
||||
fun deleteSyncVersionsOlderThan(
|
||||
@Param("documentId") documentId: UUID,
|
||||
@Param("cutoff") cutoff: OffsetDateTime,
|
||||
): Int
|
||||
}
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
package de.werkbaum.repository
|
||||
|
||||
import de.werkbaum.domain.DocumentHistoryEntry
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Zugriff auf die Dokumenthistorie.
|
||||
*
|
||||
* Bewusst **gezielt** statt „lade alles und filtere in Kotlin": Mit dem
|
||||
* Live-Editing entstehen hunderte Versionen je Dokument, und jede trägt den
|
||||
* vollen Text (D76).
|
||||
*/
|
||||
interface DocumentHistoryRepository {
|
||||
fun append(entry: DocumentHistoryEntry)
|
||||
|
||||
/** Alle Einträge zu einem Dokument, älteste zuerst. */
|
||||
fun findByDocumentId(documentId: UUID): List<DocumentHistoryEntry>
|
||||
/** Gibt es zu dieser UUID überhaupt Historie? Auch für gelöschte Dokumente wahr. */
|
||||
fun exists(documentId: UUID): Boolean
|
||||
|
||||
/** Genau eine Version – oder `null`, wenn sie nie existierte oder verdichtet wurde. */
|
||||
fun findVersion(documentId: UUID, version: Long): DocumentHistoryEntry?
|
||||
|
||||
/** Der jüngste Eintrag, gleich welchen Typs (bei gelöschten Dokumenten der Tombstone). */
|
||||
fun findLatest(documentId: UUID): DocumentHistoryEntry?
|
||||
|
||||
/** Der älteste Eintrag – die Anlage des Dokuments, immer ein Meilenstein. */
|
||||
fun findOldest(documentId: UUID): DocumentHistoryEntry?
|
||||
|
||||
/** Höchste vergebene Versionsnummer, auch wenn deren Eintrag verdichtet wurde. */
|
||||
fun maxVersion(documentId: UUID): Long?
|
||||
|
||||
/** Die nutzersichtbare Historie, älteste zuerst. */
|
||||
fun findMilestones(documentId: UUID): List<DocumentHistoryEntry>
|
||||
|
||||
/**
|
||||
* Erhebt eine Sync-Version nachträglich zum Meilenstein – sie war die
|
||||
* letzte vor einer Schreibpause.
|
||||
*/
|
||||
fun promoteToMilestone(documentId: UUID, version: Long)
|
||||
|
||||
/**
|
||||
* Verdichtet: entfernt Sync-Versionen dieses Dokuments, die älter als
|
||||
* [olderThan] sind. Meilensteine bleiben. Liefert die Zahl der entfernten
|
||||
* Einträge.
|
||||
*/
|
||||
fun compact(documentId: UUID, olderThan: OffsetDateTime): Int
|
||||
|
||||
fun clear()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import de.werkbaum.repository.DocumentRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@@ -17,6 +18,7 @@ class DocumentService(
|
||||
private val repository: DocumentRepository,
|
||||
private val historyRepository: DocumentHistoryRepository,
|
||||
private val clock: Clock,
|
||||
private val properties: LiveEditingProperties,
|
||||
) {
|
||||
|
||||
fun findAll(): List<Document> = repository.findAll()
|
||||
@@ -39,7 +41,16 @@ class DocumentService(
|
||||
return document
|
||||
}
|
||||
|
||||
fun update(id: UUID, title: String, content: String): Document {
|
||||
/**
|
||||
* Ersetzt Titel und Inhalt vollständig.
|
||||
*
|
||||
* [milestone] `false` schreibt eine **Sync-Version** – gedacht für den
|
||||
* getakteten Strom des Live-Editings (D76), der sonst hunderte
|
||||
* nutzersichtbare Stände je Sitzung erzeugte. Der Vollersatz über die API
|
||||
* ist dagegen eine bewusste Handlung (Import, Reparatur) und bleibt
|
||||
* Meilenstein.
|
||||
*/
|
||||
fun update(id: UUID, title: String, content: String, milestone: Boolean = true): Document {
|
||||
val existing = findById(id)
|
||||
val updated = existing.copy(
|
||||
title = title,
|
||||
@@ -48,7 +59,7 @@ class DocumentService(
|
||||
updatedAt = OffsetDateTime.now(clock),
|
||||
)
|
||||
repository.save(updated)
|
||||
recordHistory(updated, ChangeType.UPDATED)
|
||||
recordHistory(updated, ChangeType.UPDATED, milestone)
|
||||
return updated
|
||||
}
|
||||
|
||||
@@ -66,26 +77,42 @@ class DocumentService(
|
||||
}
|
||||
|
||||
/**
|
||||
* Historie eines Dokuments – funktioniert auch für bereits gelöschte
|
||||
* Dokumente. 404 nur, wenn die UUID gänzlich unbekannt ist.
|
||||
* Die **nutzersichtbare** Historie: alle Meilensteine, älteste zuerst,
|
||||
* dazu immer der jüngste Stand. Sync-Versionen bleiben draußen – sie
|
||||
* tragen das Protokoll, nicht die Erzählung (D76).
|
||||
*
|
||||
* Funktioniert auch für bereits gelöschte Dokumente; 404 nur, wenn die
|
||||
* UUID gänzlich unbekannt ist.
|
||||
*/
|
||||
fun history(id: UUID): List<DocumentHistoryEntry> {
|
||||
val entries = historyRepository.findByDocumentId(id)
|
||||
if (entries.isEmpty()) throw DocumentNotFoundException(id)
|
||||
return entries
|
||||
if (!historyRepository.exists(id)) throw DocumentNotFoundException(id)
|
||||
val milestones = historyRepository.findMilestones(id)
|
||||
// Die letzte Version einer noch laufenden Schreibphase ist noch kein
|
||||
// Meilenstein – sichtbar sein muss sie trotzdem.
|
||||
val latest = historyRepository.findLatest(id)
|
||||
return if (latest != null && milestones.none { it.version == latest.version }) {
|
||||
milestones + latest
|
||||
} else {
|
||||
milestones
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt ein Dokument unter derselben UUID wieder her.
|
||||
*
|
||||
* - Ohne [targetVersion]: letzter inhaltlicher Stand vor dem Löschen.
|
||||
* - Ohne [targetVersion]: letzter Stand vor dem Löschen ([ChangeType.RESTORED]).
|
||||
* Existiert das Dokument noch, gibt es einen Konflikt (409).
|
||||
* - Mit [targetVersion]: Inhalt dieser Version wird als neue Version
|
||||
* übernommen – funktioniert auch als Rollback für existierende Dokumente.
|
||||
* übernommen. Bei einem lebenden Dokument ist das ein Rückfall
|
||||
* ([ChangeType.ROLLED_BACK]), kein Wiederherstellen – der Client hatte
|
||||
* nie eine Sperre.
|
||||
*
|
||||
* Eine verdichtete Sync-Version ist nicht mehr anzusteuern (404). Das ist
|
||||
* die Zwei-Ebenen-Regel im Betrieb: Angeboten werden Meilensteine, und die
|
||||
* bleiben.
|
||||
*/
|
||||
fun restore(id: UUID, targetVersion: Long? = null): Document {
|
||||
val entries = historyRepository.findByDocumentId(id)
|
||||
if (entries.isEmpty()) throw DocumentNotFoundException(id)
|
||||
if (!historyRepository.exists(id)) throw DocumentNotFoundException(id)
|
||||
|
||||
val existing = repository.findById(id)
|
||||
if (existing != null && targetVersion == null) {
|
||||
@@ -95,28 +122,59 @@ class DocumentService(
|
||||
}
|
||||
|
||||
val snapshot = if (targetVersion != null) {
|
||||
entries.lastOrNull { it.version == targetVersion && it.changeType != ChangeType.DELETED }
|
||||
historyRepository.findVersion(id, targetVersion)
|
||||
?.takeIf { it.changeType != ChangeType.DELETED }
|
||||
?: throw DocumentNotFoundException(id)
|
||||
} else {
|
||||
entries.last { it.changeType != ChangeType.DELETED }
|
||||
// Der Tombstone trägt den letzten Stand – er ist die verlässliche
|
||||
// Quelle, auch wenn die Version davor längst verdichtet wurde.
|
||||
historyRepository.findLatest(id) ?: throw DocumentNotFoundException(id)
|
||||
}
|
||||
|
||||
val now = OffsetDateTime.now(clock)
|
||||
val lastVersion = maxOf(entries.maxOf { it.version }, existing?.version ?: 0)
|
||||
val lastVersion = maxOf(historyRepository.maxVersion(id) ?: 0, existing?.version ?: 0)
|
||||
val restored = Document(
|
||||
id = id,
|
||||
title = snapshot.title,
|
||||
content = snapshot.content,
|
||||
version = lastVersion + 1,
|
||||
createdAt = existing?.createdAt ?: entries.first().timestamp,
|
||||
createdAt = existing?.createdAt
|
||||
?: historyRepository.findOldest(id)?.timestamp
|
||||
?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
repository.save(restored)
|
||||
recordHistory(restored, ChangeType.RESTORED)
|
||||
recordHistory(
|
||||
restored,
|
||||
if (existing != null) ChangeType.ROLLED_BACK else ChangeType.RESTORED,
|
||||
)
|
||||
return restored
|
||||
}
|
||||
|
||||
private fun recordHistory(document: Document, changeType: ChangeType) {
|
||||
/**
|
||||
* Schreibt einen Historieneintrag und hält dabei die zwei Ebenen instand:
|
||||
*
|
||||
* 1. War die vorige Version eine Sync-Version und liegt sie länger als
|
||||
* [LiveEditingProperties.milestonePause] zurück, war sie die **letzte
|
||||
* vor einer Schreibpause** und wird nachträglich Meilenstein. So
|
||||
* braucht es keinen Zeitgeber – die nächste Änderung stellt fest, dass
|
||||
* eine Pause war.
|
||||
* 2. Strukturelle Änderungen sind immer Meilensteine.
|
||||
* 3. Danach wird verdichtet: Sync-Versionen jenseits der
|
||||
* Aufbewahrungsfrist entfallen.
|
||||
*/
|
||||
private fun recordHistory(
|
||||
document: Document,
|
||||
changeType: ChangeType,
|
||||
milestone: Boolean = true,
|
||||
) {
|
||||
val previous = historyRepository.findLatest(document.id)
|
||||
if (previous != null && !previous.milestone &&
|
||||
Duration.between(previous.timestamp, document.updatedAt) >= properties.milestonePause
|
||||
) {
|
||||
historyRepository.promoteToMilestone(document.id, previous.version)
|
||||
}
|
||||
|
||||
historyRepository.append(
|
||||
DocumentHistoryEntry(
|
||||
documentId = document.id,
|
||||
@@ -125,7 +183,17 @@ class DocumentService(
|
||||
content = document.content,
|
||||
changeType = changeType,
|
||||
timestamp = document.updatedAt,
|
||||
milestone = milestone || changeType.isStructural,
|
||||
)
|
||||
)
|
||||
|
||||
historyRepository.compact(
|
||||
document.id,
|
||||
document.updatedAt.minus(properties.syncRetention),
|
||||
)
|
||||
}
|
||||
|
||||
/** Anlegen, Löschen, Wiederherstellen und Rückfall sind nie bloß Sync-Versionen. */
|
||||
private val ChangeType.isStructural: Boolean
|
||||
get() = this != ChangeType.UPDATED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.werkbaum.service
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Stellschrauben des Live-Editings (D76). Bewusst konfigurierbar: Die Werte
|
||||
* sind gesetzt, nicht hergeleitet, und werden nach Erfahrung justiert – wie
|
||||
* die Schwellen im Frontend (D64, D71).
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "werkbaum.live-editing")
|
||||
data class LiveEditingProperties(
|
||||
|
||||
/**
|
||||
* Schreibpause, nach der die letzte Version zum Meilenstein wird. Kürzer
|
||||
* heißt mehr nutzersichtbare Stände, länger heißt gröbere Historie.
|
||||
*/
|
||||
val milestonePause: Duration = Duration.ofSeconds(30),
|
||||
|
||||
/**
|
||||
* Wie lange Sync-Versionen aufgehoben werden. Danach beantwortet der Feed
|
||||
* ein so altes `since` mit Volltext statt mit einem Diff.
|
||||
*/
|
||||
val syncRetention: Duration = Duration.ofHours(1),
|
||||
)
|
||||
@@ -18,5 +18,13 @@ spring:
|
||||
liquibase:
|
||||
change-log: classpath:db/changelog/db.changelog-master.sql
|
||||
|
||||
werkbaum:
|
||||
live-editing:
|
||||
# Schreibpause, nach der die letzte Version zum Meilenstein wird.
|
||||
milestone-pause: 30s
|
||||
# Danach wird eine Sync-Version verdichtet; der Feed antwortet auf ein so
|
||||
# altes "since" dann mit Volltext statt mit einem Diff.
|
||||
sync-retention: 1h
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
@@ -26,3 +26,13 @@ CREATE TABLE document_history (
|
||||
--changeset editor:003-index-document-history
|
||||
CREATE INDEX idx_document_history_document_id ON document_history (document_id);
|
||||
--rollback DROP INDEX idx_document_history_document_id;
|
||||
|
||||
--changeset editor:004-history-milestone
|
||||
-- Zwei Ebenen (D76): Meilensteine sind die nutzersichtbare Historie und
|
||||
-- bleiben; Sync-Versionen tragen die Diffs des Live-Editings und werden nach
|
||||
-- einer Weile verdichtet. Bestand ist Meilenstein - er stammt aus der Zeit
|
||||
-- ohne Live-Editing und ist durchweg nutzersichtbar.
|
||||
ALTER TABLE document_history ADD COLUMN milestone BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
CREATE INDEX idx_document_history_version ON document_history (document_id, version);
|
||||
--rollback DROP INDEX idx_document_history_version;
|
||||
--rollback ALTER TABLE document_history DROP COLUMN milestone;
|
||||
|
||||
@@ -127,11 +127,13 @@ paths:
|
||||
operationId: getDocumentHistory
|
||||
summary: Historie eines Dokuments abrufen
|
||||
description: >
|
||||
Liefert alle Versionen eines Dokuments in chronologischer Reihenfolge.
|
||||
Die Historie bleibt auch nach dem Loeschen des Dokuments erhalten.
|
||||
Liefert die nutzersichtbaren Staende (Meilensteine) in chronologischer
|
||||
Reihenfolge, dazu immer den juengsten Stand. Kurzlebige Sync-Versionen
|
||||
des Live-Editings bleiben aussen vor. Die Historie ueberlebt das
|
||||
Loeschen des Dokuments.
|
||||
responses:
|
||||
"200":
|
||||
description: Historie des Dokuments (aelteste zuerst)
|
||||
description: Meilensteine des Dokuments (aelteste zuerst)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -253,6 +255,10 @@ components:
|
||||
description: Optional; wird spaeter fuer Optimistic Locking ausgewertet.
|
||||
|
||||
DocumentHistoryEntry:
|
||||
description: >
|
||||
Ein Stand der nutzersichtbaren Historie. Sync-Versionen des
|
||||
Live-Editings erscheinen hier nicht - sie tragen das Protokoll, nicht
|
||||
die Erzaehlung.
|
||||
type: object
|
||||
required: [documentId, version, title, content, changeType, timestamp]
|
||||
properties:
|
||||
@@ -268,7 +274,11 @@ components:
|
||||
type: string
|
||||
changeType:
|
||||
type: string
|
||||
enum: [CREATED, UPDATED, DELETED, RESTORED]
|
||||
description: >
|
||||
RESTORED heisst: ein geloeschtes Dokument ist wieder da.
|
||||
ROLLED_BACK ist der Rueckfall eines lebenden Dokuments auf eine
|
||||
aeltere Version.
|
||||
enum: [CREATED, UPDATED, DELETED, RESTORED, ROLLED_BACK]
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
Reference in New Issue
Block a user