feat(bulk-edit): add PATCH /api/documents/bulk endpoint

WRITE_ALL-gated batch endpoint that applies a partial DTO to up to 500
documents per request. Per-document failures (DOCUMENT_NOT_FOUND, etc.)
are collected into the response's errors[] without aborting the batch.
Logs an audit line consistent with quickUpload.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-25 14:34:52 +02:00
parent a59feec81a
commit f0da033ec9
2 changed files with 150 additions and 0 deletions

View File

@@ -18,7 +18,10 @@ import jakarta.validation.constraints.Min;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.validation.annotation.Validated;
import org.raddatz.familienarchiv.dto.BulkEditError;
import org.raddatz.familienarchiv.dto.BulkEditResult;
import org.raddatz.familienarchiv.dto.DocumentBatchMetadataDTO;
import org.raddatz.familienarchiv.dto.DocumentBulkEditDTO;
import org.raddatz.familienarchiv.dto.DocumentSearchResult;
import org.raddatz.familienarchiv.dto.DocumentUpdateDTO;
import org.raddatz.familienarchiv.dto.TagOperator;
@@ -237,6 +240,45 @@ public class DocumentController {
return new QuickUploadResult(created, updated, errors);
}
// --- BULK EDIT ---
private static final int BULK_EDIT_MAX_IDS = 500;
@PatchMapping("/bulk")
@RequirePermission(Permission.WRITE_ALL)
public BulkEditResult patchBulk(
@RequestBody DocumentBulkEditDTO dto,
Authentication authentication) {
if (dto.getDocumentIds() == null || dto.getDocumentIds().isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "documentIds is required");
}
if (dto.getDocumentIds().size() > BULK_EDIT_MAX_IDS) {
throw DomainException.badRequest(ErrorCode.BULK_EDIT_TOO_MANY_IDS,
"Maximum " + BULK_EDIT_MAX_IDS + " documents per request, got: " + dto.getDocumentIds().size());
}
UUID actorId = requireUserId(authentication);
int updated = 0;
List<BulkEditError> errors = new ArrayList<>();
for (UUID id : dto.getDocumentIds()) {
try {
documentService.applyBulkEditToDocument(id, dto);
updated++;
} catch (DomainException e) {
errors.add(new BulkEditError(id, e.getMessage()));
} catch (Exception e) {
errors.add(new BulkEditError(id, "Internal error"));
log.warn("Bulk edit failed for document {}: {}", id, e.getMessage());
}
}
log.info("bulkEdit actor={} documentIds={} updated={} errors={}",
actorId, dto.getDocumentIds().size(), updated, errors.size());
return new BulkEditResult(updated, errors);
}
@GetMapping("/incomplete-count")
@RequirePermission(Permission.WRITE_ALL)
public Map<String, Long> getIncompleteCount() {