From f29f4d3f5b834b3f6a5c1cc41638055d415eb7f6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 27 Apr 2026 14:22:34 +0200 Subject: [PATCH] feat(stammbaum): RelationshipController for the Stammbaum API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven endpoints in one controller, two roots: - GET /api/network → NetworkDTO - GET /api/persons/{id}/relationships → List - GET /api/persons/{id}/inferred-relationships - GET /api/persons/{aId}/relationship-to/{bId} → 200 or 404 - POST /api/persons/{id}/relationships WRITE_ALL - DEL /api/persons/{id}/relationships/{relId} WRITE_ALL, 204 - PATCH /api/persons/{id}/family-member WRITE_ALL PersonController is intentionally untouched. Controller-boundary validation via RelationType.valueOf catches unknown types as 400 before the service is invoked. FamilyMemberPatchDTO is a one-field record for the family-member toggle. Refs #358. Co-Authored-By: Claude Sonnet 4.6 --- .../relationship/RelationshipController.java | 92 +++++++++++++++++++ .../dto/FamilyMemberPatchDTO.java | 4 + 2 files changed, 96 insertions(+) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/FamilyMemberPatchDTO.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java new file mode 100644 index 00000000..1a94cc29 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java @@ -0,0 +1,92 @@ +package org.raddatz.familienarchiv.relationship; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.relationship.dto.CreateRelationshipRequest; +import org.raddatz.familienarchiv.relationship.dto.FamilyMemberPatchDTO; +import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipDTO; +import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipWithPersonDTO; +import org.raddatz.familienarchiv.relationship.dto.NetworkDTO; +import org.raddatz.familienarchiv.relationship.dto.RelationshipDTO; +import org.raddatz.familienarchiv.security.Permission; +import org.raddatz.familienarchiv.security.RequirePermission; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; +import java.util.UUID; + +/** + * Stammbaum API. Endpoints split across two roots: + *
    + *
  • {@code /api/network} — the family graph
  • + *
  • {@code /api/persons/{id}/...} — per-person relationship operations + * (PersonController is intentionally left untouched)
  • + *
+ */ +@RestController +@RequiredArgsConstructor +public class RelationshipController { + + private final RelationshipService relationshipService; + + @GetMapping("/api/network") + public NetworkDTO getNetwork() { + return relationshipService.getFamilyNetwork(); + } + + @GetMapping("/api/persons/{id}/relationships") + public List getRelationships(@PathVariable UUID id) { + return relationshipService.getRelationships(id); + } + + @GetMapping("/api/persons/{id}/inferred-relationships") + public List getInferredRelationships(@PathVariable UUID id) { + return relationshipService.getInferredRelationships(id); + } + + @GetMapping("/api/persons/{aId}/relationship-to/{bId}") + public InferredRelationshipDTO getRelationshipBetween(@PathVariable UUID aId, @PathVariable UUID bId) { + return relationshipService.getRelationshipBetween(aId, bId) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, "No relationship path between " + aId + " and " + bId)); + } + + @PostMapping("/api/persons/{id}/relationships") + @RequirePermission(Permission.WRITE_ALL) + public ResponseEntity addRelationship( + @PathVariable UUID id, + @Valid @RequestBody CreateRelationshipRequest dto) { + validateRelationType(dto.relationType()); + return ResponseEntity.status(HttpStatus.CREATED) + .body(relationshipService.addRelationship(id, dto)); + } + + @DeleteMapping("/api/persons/{id}/relationships/{relId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + @RequirePermission(Permission.WRITE_ALL) + public void deleteRelationship(@PathVariable UUID id, @PathVariable UUID relId) { + relationshipService.deleteRelationship(id, relId); + } + + @PatchMapping("/api/persons/{id}/family-member") + @RequirePermission(Permission.WRITE_ALL) + public Person patchFamilyMember(@PathVariable UUID id, @RequestBody FamilyMemberPatchDTO dto) { + return relationshipService.setFamilyMember(id, dto.familyMember()); + } + + private static void validateRelationType(String typeName) { + if (typeName == null || typeName.isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "relationType is required"); + } + try { + RelationType.valueOf(typeName); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "Unknown relationType: " + typeName); + } + } +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/FamilyMemberPatchDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/FamilyMemberPatchDTO.java new file mode 100644 index 00000000..74d0bcb5 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/FamilyMemberPatchDTO.java @@ -0,0 +1,4 @@ +package org.raddatz.familienarchiv.relationship.dto; + +/** Body for {@code PATCH /api/persons/{id}/family-member}. */ +public record FamilyMemberPatchDTO(boolean familyMember) {}