feat: add admin backfill-versions endpoint for documents without history

Adds POST /api/admin/backfill-versions which creates an initial snapshot
(editorName="Datenimport", changedFields=[]) for every document that has
no version entry yet, using the document's createdAt as the version timestamp.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-23 12:27:21 +01:00
parent f32ed32f67
commit 3e65b2feb3
7 changed files with 177 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
package org.raddatz.familienarchiv.controller;
import org.raddatz.familienarchiv.dto.BackfillResult;
import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.security.RequirePermission;
import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.service.DocumentVersionService;
import org.raddatz.familienarchiv.service.MassImportService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +21,8 @@ import lombok.RequiredArgsConstructor;
public class AdminController {
private final MassImportService massImportService;
private final DocumentService documentService;
private final DocumentVersionService documentVersionService;
@PostMapping("/trigger-import")
public ResponseEntity<MassImportService.ImportStatus> triggerMassImport() {
@@ -29,4 +34,11 @@ public class AdminController {
public ResponseEntity<MassImportService.ImportStatus> importStatus() {
return ResponseEntity.ok(massImportService.getStatus());
}
@PostMapping("/backfill-versions")
public ResponseEntity<BackfillResult> backfillVersions() {
int count = documentVersionService.backfillMissingVersions(
documentService.getDocumentsWithoutVersions());
return ResponseEntity.ok(new BackfillResult(count));
}
}

View File

@@ -0,0 +1,7 @@
package org.raddatz.familienarchiv.dto;
import io.swagger.v3.oas.annotations.media.Schema;
public record BackfillResult(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) int count
) {}

View File

@@ -34,6 +34,9 @@ public interface DocumentRepository extends JpaRepository<Document, UUID>, JpaSp
List<Document> findByTags_Id(UUID tagId);
@Query("SELECT d FROM Document d WHERE d.id NOT IN (SELECT DISTINCT dv.documentId FROM DocumentVersion dv)")
List<Document> findDocumentsWithoutVersions();
@Query("SELECT DISTINCT d FROM Document d " +
"JOIN d.receivers r " +
"WHERE " +

View File

@@ -257,6 +257,10 @@ public class DocumentService {
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
}
public List<Document> getDocumentsWithoutVersions() {
return documentRepository.findDocumentsWithoutVersions();
}
public List<Document> getDocumentsBySender(UUID senderId) {
return documentRepository.findBySenderId(senderId);
}

View File

@@ -56,6 +56,26 @@ public class DocumentVersionService {
.build());
}
@Transactional
public int backfillMissingVersions(List<Document> docs) {
int count = 0;
for (Document doc : docs) {
List<DocumentVersion> existing = versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId());
if (!existing.isEmpty()) continue;
LocalDateTime savedAt = doc.getCreatedAt() != null ? doc.getCreatedAt() : LocalDateTime.now();
versionRepository.save(DocumentVersion.builder()
.documentId(doc.getId())
.savedAt(savedAt)
.editorId(null)
.editorName("Datenimport")
.snapshot(serializeSnapshot(doc))
.changedFields("[]")
.build());
count++;
}
return count;
}
public List<DocumentVersionSummary> getSummaries(UUID documentId) {
return versionRepository.findByDocumentIdOrderBySavedAtAsc(documentId).stream()
.map(v -> new DocumentVersionSummary(