refactor(relationship): use typed RelationType enum in CreateRelationshipRequest #365
@@ -86,10 +86,9 @@ public class RelationshipService {
|
|||||||
Person person = personService.getById(personId);
|
Person person = personService.getById(personId);
|
||||||
Person relatedPerson = personService.getById(dto.relatedPersonId());
|
Person relatedPerson = personService.getById(dto.relatedPersonId());
|
||||||
|
|
||||||
RelationType type = parseType(dto.relationType());
|
|
||||||
validateYears(dto.fromYear(), dto.toYear());
|
validateYears(dto.fromYear(), dto.toYear());
|
||||||
|
|
||||||
if (type == RelationType.PARENT_OF
|
if (dto.relationType() == RelationType.PARENT_OF
|
||||||
&& relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
&& relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
relatedPerson.getId(), personId, RelationType.PARENT_OF)) {
|
relatedPerson.getId(), personId, RelationType.PARENT_OF)) {
|
||||||
throw DomainException.conflict(
|
throw DomainException.conflict(
|
||||||
@@ -100,7 +99,7 @@ public class RelationshipService {
|
|||||||
PersonRelationship rel = PersonRelationship.builder()
|
PersonRelationship rel = PersonRelationship.builder()
|
||||||
.person(person)
|
.person(person)
|
||||||
.relatedPerson(relatedPerson)
|
.relatedPerson(relatedPerson)
|
||||||
.relationType(type)
|
.relationType(dto.relationType())
|
||||||
.fromYear(dto.fromYear())
|
.fromYear(dto.fromYear())
|
||||||
.toYear(dto.toYear())
|
.toYear(dto.toYear())
|
||||||
.notes(blankToNull(dto.notes()))
|
.notes(blankToNull(dto.notes()))
|
||||||
@@ -113,7 +112,7 @@ public class RelationshipService {
|
|||||||
} catch (DataIntegrityViolationException e) {
|
} catch (DataIntegrityViolationException e) {
|
||||||
throw DomainException.conflict(
|
throw DomainException.conflict(
|
||||||
ErrorCode.DUPLICATE_RELATIONSHIP,
|
ErrorCode.DUPLICATE_RELATIONSHIP,
|
||||||
"Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + type + ")");
|
"Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + dto.relationType() + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,18 +140,6 @@ public class RelationshipService {
|
|||||||
return (s == null || s.isBlank()) ? null : s.trim();
|
return (s == null || s.isBlank()) ? null : s.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RelationType parseType(String typeName) {
|
|
||||||
if (typeName == null) {
|
|
||||||
throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "relationType is required");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return RelationType.valueOf(typeName);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw DomainException.badRequest(
|
|
||||||
ErrorCode.VALIDATION_ERROR, "Invalid relationType: " + typeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void validateYears(Integer fromYear, Integer toYear) {
|
private static void validateYears(Integer fromYear, Integer toYear) {
|
||||||
if (fromYear != null && toYear != null && toYear < fromYear) {
|
if (fromYear != null && toYear != null && toYear < fromYear) {
|
||||||
throw DomainException.badRequest(
|
throw DomainException.badRequest(
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
package org.raddatz.familienarchiv.relationship.dto;
|
package org.raddatz.familienarchiv.relationship.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.raddatz.familienarchiv.relationship.RelationType;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
|
||||||
* POST body for {@code /api/persons/{id}/relationships}. {@code relationType}
|
|
||||||
* is a string here; the controller validates it against the {@code RelationType}
|
|
||||||
* enum at the boundary.
|
|
||||||
*/
|
|
||||||
public record CreateRelationshipRequest(
|
public record CreateRelationshipRequest(
|
||||||
@NotNull UUID relatedPersonId,
|
@NotNull UUID relatedPersonId,
|
||||||
@NotBlank String relationType,
|
@NotNull RelationType relationType,
|
||||||
Integer fromYear,
|
Integer fromYear,
|
||||||
Integer toYear,
|
Integer toYear,
|
||||||
@Size(max = 2000) String notes
|
@Size(max = 2000) String notes
|
||||||
|
|||||||
@@ -122,6 +122,15 @@ class RelationshipControllerTest {
|
|||||||
.andExpect(jsonPath("$[0].hops").value(2));
|
.andExpect(jsonPath("$[0].hops").value(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
|
||||||
|
void addRelationship_returns400_when_relationType_is_unknown_value() throws Exception {
|
||||||
|
mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"NOT_A_REAL_TYPE\"}"))
|
||||||
|
.andExpect(status().isBadRequest());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
|
@WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
|
||||||
void addRelationship_returns201_with_RelationshipDTO_for_WRITE_ALL_user() throws Exception {
|
void addRelationship_returns201_with_RelationshipDTO_for_WRITE_ALL_user() throws Exception {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addRelationship_stores_and_is_readable() {
|
void addRelationship_stores_and_is_readable() {
|
||||||
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, null);
|
var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, 1900, null, null);
|
||||||
|
|
||||||
RelationshipDTO created = relationshipService.addRelationship(alice.getId(), dto);
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(), dto);
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addRelationship_throws_409_when_duplicate() {
|
void addRelationship_throws_409_when_duplicate() {
|
||||||
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null);
|
var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null);
|
||||||
relationshipService.addRelationship(alice.getId(), dto);
|
relationshipService.addRelationship(alice.getId(), dto);
|
||||||
|
|
||||||
assertThatThrownBy(() -> relationshipService.addRelationship(alice.getId(), dto))
|
assertThatThrownBy(() -> relationshipService.addRelationship(alice.getId(), dto))
|
||||||
@@ -93,9 +93,9 @@ class RelationshipServiceIntegrationTest {
|
|||||||
void addRelationship_throws_409_when_circular_parent() {
|
void addRelationship_throws_409_when_circular_parent() {
|
||||||
// alice PARENT_OF bob; now try bob PARENT_OF alice → must be rejected.
|
// alice PARENT_OF bob; now try bob PARENT_OF alice → must be rejected.
|
||||||
relationshipService.addRelationship(alice.getId(),
|
relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null));
|
||||||
|
|
||||||
var reverse = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null);
|
var reverse = new CreateRelationshipRequest(alice.getId(), RelationType.PARENT_OF, null, null, null);
|
||||||
assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
|
assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -105,7 +105,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void deleteRelationship_throws_403_when_rel_belongs_to_different_person() {
|
void deleteRelationship_throws_403_when_rel_belongs_to_different_person() {
|
||||||
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null));
|
||||||
|
|
||||||
// Charlie is unrelated to this row.
|
// Charlie is unrelated to this row.
|
||||||
assertThatThrownBy(() -> relationshipService.deleteRelationship(charlie.getId(), created.id()))
|
assertThatThrownBy(() -> relationshipService.deleteRelationship(charlie.getId(), created.id()))
|
||||||
@@ -122,9 +122,9 @@ class RelationshipServiceIntegrationTest {
|
|||||||
// V55 enforces symmetric uniqueness for SPOUSE_OF. Inserting (alice, bob, SPOUSE_OF)
|
// V55 enforces symmetric uniqueness for SPOUSE_OF. Inserting (alice, bob, SPOUSE_OF)
|
||||||
// and then (bob, alice, SPOUSE_OF) must be rejected, just like reverse SIBLING_OF.
|
// and then (bob, alice, SPOUSE_OF) must be rejected, just like reverse SIBLING_OF.
|
||||||
relationshipService.addRelationship(alice.getId(),
|
relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null));
|
new CreateRelationshipRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null));
|
||||||
|
|
||||||
var reverse = new CreateRelationshipRequest(alice.getId(), "SPOUSE_OF", null, null, null);
|
var reverse = new CreateRelationshipRequest(alice.getId(), RelationType.SPOUSE_OF, null, null, null);
|
||||||
assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
|
assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -135,7 +135,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
void deleteRelationship_succeeds_for_symmetric_type_from_either_side() {
|
void deleteRelationship_succeeds_for_symmetric_type_from_either_side() {
|
||||||
// alice SPOUSE_OF bob. Bob deletes from his side.
|
// alice SPOUSE_OF bob. Bob deletes from his side.
|
||||||
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null));
|
new CreateRelationshipRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null));
|
||||||
|
|
||||||
relationshipService.deleteRelationship(bob.getId(), created.id());
|
relationshipService.deleteRelationship(bob.getId(), created.id());
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
// charlie starts with familyMember = false. Add a PARENT_OF edge alice→charlie
|
// charlie starts with familyMember = false. Add a PARENT_OF edge alice→charlie
|
||||||
// so the edge exists, then flip charlie's flag and verify he appears in nodes.
|
// so the edge exists, then flip charlie's flag and verify he appears in nodes.
|
||||||
relationshipService.addRelationship(alice.getId(),
|
relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(charlie.getId(), "PARENT_OF", null, null, null));
|
new CreateRelationshipRequest(charlie.getId(), RelationType.PARENT_OF, null, null, null));
|
||||||
|
|
||||||
NetworkDTO before = relationshipService.getFamilyNetwork();
|
NetworkDTO before = relationshipService.getFamilyNetwork();
|
||||||
assertThat(before.nodes()).extracting("id").doesNotContain(charlie.getId());
|
assertThat(before.nodes()).extracting("id").doesNotContain(charlie.getId());
|
||||||
@@ -163,7 +163,7 @@ class RelationshipServiceIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void delete_person_cascades_to_relationships() {
|
void delete_person_cascades_to_relationships() {
|
||||||
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
RelationshipDTO created = relationshipService.addRelationship(alice.getId(),
|
||||||
new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null));
|
new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null));
|
||||||
UUID relId = created.id();
|
UUID relId = created.id();
|
||||||
assertThat(relationshipRepository.findById(relId)).isPresent();
|
assertThat(relationshipRepository.findById(relId)).isPresent();
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class RelationshipServiceTest {
|
|||||||
when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType(
|
||||||
alice.getId(), bob.getId(), RelationType.PARENT_OF)).thenReturn(true);
|
alice.getId(), bob.getId(), RelationType.PARENT_OF)).thenReturn(true);
|
||||||
|
|
||||||
var dto = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null);
|
var dto = new CreateRelationshipRequest(alice.getId(), RelationType.PARENT_OF, null, null, null);
|
||||||
assertThatThrownBy(() -> service.addRelationship(bob.getId(), dto))
|
assertThatThrownBy(() -> service.addRelationship(bob.getId(), dto))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -96,7 +96,7 @@ class RelationshipServiceTest {
|
|||||||
bob.getId(), alice.getId(), RelationType.PARENT_OF)).thenReturn(false);
|
bob.getId(), alice.getId(), RelationType.PARENT_OF)).thenReturn(false);
|
||||||
when(relationshipRepository.saveAndFlush(any())).thenThrow(new DataIntegrityViolationException("unique_rel"));
|
when(relationshipRepository.saveAndFlush(any())).thenThrow(new DataIntegrityViolationException("unique_rel"));
|
||||||
|
|
||||||
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null);
|
var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null);
|
||||||
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -105,7 +105,7 @@ class RelationshipServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addRelationship_throws_BAD_REQUEST_when_self_relationship() {
|
void addRelationship_throws_BAD_REQUEST_when_self_relationship() {
|
||||||
var dto = new CreateRelationshipRequest(alice.getId(), "FRIEND", null, null, null);
|
var dto = new CreateRelationshipRequest(alice.getId(), RelationType.FRIEND, null, null, null);
|
||||||
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -117,7 +117,7 @@ class RelationshipServiceTest {
|
|||||||
void addRelationship_throws_BAD_REQUEST_when_to_year_before_from_year() {
|
void addRelationship_throws_BAD_REQUEST_when_to_year_before_from_year() {
|
||||||
when(personService.getById(alice.getId())).thenReturn(alice);
|
when(personService.getById(alice.getId())).thenReturn(alice);
|
||||||
when(personService.getById(bob.getId())).thenReturn(bob);
|
when(personService.getById(bob.getId())).thenReturn(bob);
|
||||||
var dto = new CreateRelationshipRequest(bob.getId(), "FRIEND", 1950, 1940, null);
|
var dto = new CreateRelationshipRequest(bob.getId(), RelationType.FRIEND, 1950, 1940, null);
|
||||||
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting("code")
|
.extracting("code")
|
||||||
@@ -138,7 +138,7 @@ class RelationshipServiceTest {
|
|||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
|
|
||||||
var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, "first born");
|
var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, 1900, null, "first born");
|
||||||
var result = service.addRelationship(alice.getId(), dto);
|
var result = service.addRelationship(alice.getId(), dto);
|
||||||
|
|
||||||
assertThat(result.personId()).isEqualTo(alice.getId());
|
assertThat(result.personId()).isEqualTo(alice.getId());
|
||||||
|
|||||||
Reference in New Issue
Block a user