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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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 " +
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.raddatz.familienarchiv.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.config.SecurityConfig;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.security.PermissionAspect;
|
||||
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
|
||||
import org.raddatz.familienarchiv.service.DocumentService;
|
||||
import org.raddatz.familienarchiv.service.DocumentVersionService;
|
||||
import org.raddatz.familienarchiv.service.MassImportService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(AdminController.class)
|
||||
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
|
||||
class AdminControllerTest {
|
||||
|
||||
@Autowired MockMvc mockMvc;
|
||||
|
||||
@MockitoBean MassImportService massImportService;
|
||||
@MockitoBean DocumentService documentService;
|
||||
@MockitoBean DocumentVersionService documentVersionService;
|
||||
@MockitoBean CustomUserDetailsService customUserDetailsService;
|
||||
|
||||
@Test
|
||||
void backfillVersions_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(post("/api/admin/backfill-versions"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(roles = "USER")
|
||||
void backfillVersions_returns403_whenNotAdmin() throws Exception {
|
||||
mockMvc.perform(post("/api/admin/backfill-versions"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "ADMIN")
|
||||
void backfillVersions_returns200_withCount_whenAdmin() throws Exception {
|
||||
when(documentService.getDocumentsWithoutVersions()).thenReturn(List.of(Document.builder().build()));
|
||||
when(documentVersionService.backfillMissingVersions(anyList())).thenReturn(1);
|
||||
|
||||
mockMvc.perform(post("/api/admin/backfill-versions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.count").value(1));
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,76 @@ class DocumentVersionServiceTest {
|
||||
.isInstanceOf(DomainException.class);
|
||||
}
|
||||
|
||||
// ─── backfillMissingVersions ──────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void backfill_createsVersion_withEditorNameDatenimport() {
|
||||
Document doc = minimalDocument();
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
|
||||
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
versionService.backfillMissingVersions(List.of(doc));
|
||||
|
||||
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
|
||||
verify(versionRepository).save(captor.capture());
|
||||
assertThat(captor.getValue().getEditorName()).isEqualTo("Datenimport");
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_usesDocumentCreatedAt_asSavedAt() {
|
||||
LocalDateTime createdAt = LocalDateTime.of(2020, 3, 15, 10, 0);
|
||||
Document doc = Document.builder()
|
||||
.id(UUID.randomUUID()).title("T").createdAt(createdAt).build();
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
|
||||
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
versionService.backfillMissingVersions(List.of(doc));
|
||||
|
||||
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
|
||||
verify(versionRepository).save(captor.capture());
|
||||
assertThat(captor.getValue().getSavedAt()).isEqualTo(createdAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_setsChangedFieldsEmpty() {
|
||||
Document doc = minimalDocument();
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
|
||||
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
versionService.backfillMissingVersions(List.of(doc));
|
||||
|
||||
ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
|
||||
verify(versionRepository).save(captor.capture());
|
||||
assertThat(captor.getValue().getChangedFields()).isEqualTo("[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_skipsDocuments_thatAlreadyHaveVersions() {
|
||||
Document doc = minimalDocument();
|
||||
DocumentVersion existing = DocumentVersion.builder()
|
||||
.id(UUID.randomUUID()).documentId(doc.getId()).snapshot("{}")
|
||||
.changedFields("[]").editorName("user").savedAt(LocalDateTime.now()).build();
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of(existing));
|
||||
|
||||
int count = versionService.backfillMissingVersions(List.of(doc));
|
||||
|
||||
verify(versionRepository, never()).save(any());
|
||||
assertThat(count).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_returnsCountOfCreatedVersions() {
|
||||
Document d1 = minimalDocument();
|
||||
Document d2 = minimalDocument();
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(d1.getId())).thenReturn(List.of());
|
||||
when(versionRepository.findByDocumentIdOrderBySavedAtAsc(d2.getId())).thenReturn(List.of());
|
||||
when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
int count = versionService.backfillMissingVersions(List.of(d1, d2));
|
||||
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void authenticateAs(String username) {
|
||||
|
||||
Reference in New Issue
Block a user