feat(person): paginate GET /api/persons and add confirm/delete endpoints
GET /api/persons now returns PersonSearchResult with server-side filter params
(type, familyOnly, hasDocuments, provisional) and page/size bounds (@Min/@Max
-> 400). review=true drops the clean reader default. The legacy
sort=documentCount top-N path is folded into the paged contract. Add
PATCH /{id}/confirm and DELETE /{id}, both WRITE_ALL-guarded. Remove the now
unreachable PersonService.findAll(String).
BREAKING-CHANGE: GET /api/persons response shape changes from a bare list to
PersonSearchResult { items, totalElements, pageNumber, pageSize, totalPages }.
Refs #667
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,15 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/persons")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
public class PersonController {
|
||||
|
||||
private final PersonService personService;
|
||||
@@ -35,15 +38,35 @@ public class PersonController {
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(Permission.READ_ALL)
|
||||
public ResponseEntity<List<PersonSummaryDTO>> getPersons(
|
||||
public ResponseEntity<PersonSearchResult> getPersons(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false, defaultValue = "0") int size,
|
||||
@RequestParam(required = false) String sort) {
|
||||
if ("documentCount".equals(sort) && size > 0 && q == null) {
|
||||
@RequestParam(required = false) PersonType type,
|
||||
@RequestParam(required = false) Boolean familyOnly,
|
||||
@RequestParam(required = false) Boolean hasDocuments,
|
||||
@RequestParam(required = false) Boolean provisional,
|
||||
// review=true reveals the import noise (transcriber view); absent/false keeps the
|
||||
// clean reader default (familyMember OR documentCount > 0). The explicit filters AND
|
||||
// within whichever base the review flag selects.
|
||||
@RequestParam(required = false, defaultValue = "false") boolean review,
|
||||
@RequestParam(required = false) String sort,
|
||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
||||
@RequestParam(defaultValue = "50") @Min(1) @Max(100) int size) {
|
||||
// Legacy top-N-by-document-count path (reader dashboard): preserved, now wrapped in the
|
||||
// paged contract so /api/persons always returns one shape.
|
||||
if ("documentCount".equals(sort) && q == null) {
|
||||
int safeSize = Math.min(size, 50);
|
||||
return ResponseEntity.ok(personService.findTopByDocumentCount(safeSize));
|
||||
List<PersonSummaryDTO> top = personService.findTopByDocumentCount(safeSize);
|
||||
return ResponseEntity.ok(PersonSearchResult.paged(top, 0, safeSize, top.size()));
|
||||
}
|
||||
return ResponseEntity.ok(personService.findAll(q));
|
||||
|
||||
PersonFilter filter = PersonFilter.builder()
|
||||
.type(type)
|
||||
.familyOnly(familyOnly)
|
||||
.hasDocuments(hasDocuments)
|
||||
.provisional(provisional)
|
||||
.readerDefault(!review)
|
||||
.build();
|
||||
return ResponseEntity.ok(personService.search(filter, page, size, q));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@@ -110,6 +133,21 @@ public class PersonController {
|
||||
personService.mergePersons(id, UUID.fromString(targetIdStr));
|
||||
}
|
||||
|
||||
// Dedicated state transition that clears the provisional flag. A separate verb (not a
|
||||
// mass-assignable DTO field) so provisional can never be smuggled in via create/update.
|
||||
@PatchMapping("/{id}/confirm")
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public ResponseEntity<Person> confirmPerson(@PathVariable UUID id) {
|
||||
return ResponseEntity.ok(personService.confirmPerson(id));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@RequirePermission(Permission.WRITE_ALL)
|
||||
public void deletePerson(@PathVariable UUID id) {
|
||||
personService.deletePerson(id);
|
||||
}
|
||||
|
||||
// ─── Alias endpoints ────────────────────────────────────────────────────
|
||||
|
||||
@GetMapping("/{id}/aliases")
|
||||
|
||||
@@ -31,16 +31,6 @@ public class PersonService {
|
||||
private final PersonRepository personRepository;
|
||||
private final PersonNameAliasRepository aliasRepository;
|
||||
|
||||
public List<PersonSummaryDTO> findAll(String q) {
|
||||
if (q == null) {
|
||||
return personRepository.findAllWithDocumentCount();
|
||||
}
|
||||
if (q.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return personRepository.searchWithDocumentCount(q.trim());
|
||||
}
|
||||
|
||||
public List<PersonSummaryDTO> findTopByDocumentCount(int limit) {
|
||||
return personRepository.findTopByDocumentCount(limit);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user