refactor: enforce Controller → Service → Repository layering throughout backend

- Created TagService: encapsulates all tag find/create/update/delete operations
- Extended PersonService: added findAll(), getById(), getAllById(), findOrCreateByAlias()
- Extended UserService: added createGroup(), updateGroup(), deleteGroup(), getGroupById()
- DocumentService: replaced direct PersonRepository/TagRepository access with
  PersonService/TagService calls; added getDocumentById(), getDocumentsBySender(),
  getConversationFiltered(), deleteTagCascading()
- MassImportService: replaced PersonRepository/TagRepository with PersonService/TagService
- PersonController: removed direct repo injections, delegates to PersonService/DocumentService
- DocumentController: removed DocumentRepository injection, delegates to DocumentService
- TagController: removed TagRepository/DocumentRepository, delegates to TagService/DocumentService
- GroupController: removed UserGroupRepository injection, delegates to UserService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-17 08:49:33 +01:00
parent 97e5255d7f
commit 25e095ea47
9 changed files with 171 additions and 117 deletions

View File

@@ -9,16 +9,15 @@ import org.raddatz.familienarchiv.dto.DocumentUpdateDTO;
import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.security.RequirePermission;
import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.service.FileService; import org.raddatz.familienarchiv.service.FileService;
import org.springframework.core.io.InputStreamResource;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -39,26 +38,21 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class DocumentController { public class DocumentController {
private final DocumentRepository documentRepository;
private final DocumentService documentService; private final DocumentService documentService;
private final FileService fileService; private final FileService fileService;
// --- DOWNLOAD --- // --- DOWNLOAD ---
@GetMapping("/{id}/file") @GetMapping("/{id}/file")
public ResponseEntity<InputStreamResource> getDocumentFile(@PathVariable UUID id) { public ResponseEntity<InputStreamResource> getDocumentFile(@PathVariable UUID id) {
// 1. Look up path in DB Document doc = documentService.getDocumentById(id);
Document doc = documentRepository.findById(id)
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
if (doc.getFilePath() == null) { if (doc.getFilePath() == null) {
throw DomainException.notFound(ErrorCode.DOCUMENT_NO_FILE, "Document has no file attached: " + id); throw DomainException.notFound(ErrorCode.DOCUMENT_NO_FILE, "Document has no file attached: " + id);
} }
// 2. Delegate Retrieval to FileService
try { try {
FileService.S3FileDownload download = fileService.downloadFile(doc.getFilePath()); FileService.S3FileDownload download = fileService.downloadFile(doc.getFilePath());
// Prefer the content type stored at upload time; fall back to whatever S3 reports
String contentType = (doc.getContentType() != null && !doc.getContentType().isBlank()) String contentType = (doc.getContentType() != null && !doc.getContentType().isBlank())
? doc.getContentType() ? doc.getContentType()
: download.contentType(); : download.contentType();
@@ -75,8 +69,7 @@ public class DocumentController {
// --- METADATA --- // --- METADATA ---
@GetMapping("/{id}") @GetMapping("/{id}")
public Document getDocument(@PathVariable UUID id) { public Document getDocument(@PathVariable UUID id) {
return documentRepository.findById(id) return documentService.getDocumentById(id);
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
} }
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@@ -95,7 +88,7 @@ public class DocumentController {
@RequirePermission(Permission.WRITE_ALL) @RequirePermission(Permission.WRITE_ALL)
public Document updateDocument( public Document updateDocument(
@PathVariable UUID id, @PathVariable UUID id,
@ModelAttribute DocumentUpdateDTO dto, // Bindet Form-Felder automatisch @ModelAttribute DocumentUpdateDTO dto,
@RequestPart(value = "file", required = false) MultipartFile file) { @RequestPart(value = "file", required = false) MultipartFile file) {
try { try {
return documentService.updateDocument(id, dto, file); return documentService.updateDocument(id, dto, file);
@@ -121,18 +114,8 @@ public class DocumentController {
@RequestParam UUID receiverId, @RequestParam UUID receiverId,
@RequestParam(required = false) LocalDate from, @RequestParam(required = false) LocalDate from,
@RequestParam(required = false) LocalDate to, @RequestParam(required = false) LocalDate to,
@RequestParam(defaultValue = "DESC") String dir // ASC oder DESC @RequestParam(defaultValue = "DESC") String dir) {
) { Sort sort = Sort.by(Sort.Direction.fromString(dir.toUpperCase()), "documentDate");
// 1. Standard-Datumswerte setzen return documentService.getConversationFiltered(senderId, receiverId, from, to, sort);
LocalDate dateFrom = (from != null) ? from : LocalDate.parse("0000-01-01");
LocalDate dateTo = (to != null) ? to : LocalDate.now();
// 2. Sortierung
Sort.Direction direction = Sort.Direction.fromString(dir.toUpperCase());
Sort sort = Sort.by(direction, "documentDate");
// 3. Abfrage
return documentRepository.findConversation(
senderId, receiverId, dateFrom, dateTo, sort);
} }
} }

View File

@@ -5,12 +5,9 @@ import java.util.UUID;
import org.raddatz.familienarchiv.dto.GroupDTO; import org.raddatz.familienarchiv.dto.GroupDTO;
import org.raddatz.familienarchiv.model.UserGroup; import org.raddatz.familienarchiv.model.UserGroup;
import org.raddatz.familienarchiv.repository.UserGroupRepository;
import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.security.RequirePermission;
import org.raddatz.familienarchiv.service.UserService; import org.raddatz.familienarchiv.service.UserService;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -28,33 +25,22 @@ import lombok.RequiredArgsConstructor;
@RequirePermission(Permission.ADMIN_PERMISSION) @RequirePermission(Permission.ADMIN_PERMISSION)
@RequiredArgsConstructor @RequiredArgsConstructor
public class GroupController { public class GroupController {
private final UserGroupRepository groupRepository;
private final UserService userService; private final UserService userService;
@PostMapping("") @PostMapping("")
public ResponseEntity<UserGroup> createGroup(@RequestBody GroupDTO dto) { public ResponseEntity<UserGroup> createGroup(@RequestBody GroupDTO dto) {
UserGroup group = new UserGroup(); return ResponseEntity.ok(userService.createGroup(dto));
group.setName(dto.getName());
group.setPermissions(dto.getPermissions()); // Assuming entity has Set<String> or Set<Enum>
return ResponseEntity.ok(groupRepository.save(group));
} }
@PatchMapping("/{id}") @PatchMapping("/{id}")
public ResponseEntity<UserGroup> updateGroup(@PathVariable UUID id, @RequestBody GroupDTO dto) { public ResponseEntity<UserGroup> updateGroup(@PathVariable UUID id, @RequestBody GroupDTO dto) {
UserGroup group = groupRepository.findById(id) return ResponseEntity.ok(userService.updateGroup(id, dto));
.orElseThrow(() -> DomainException.notFound(ErrorCode.INTERNAL_ERROR, "Group not found: " + id));
if (dto.getName() != null)
group.setName(dto.getName());
if (dto.getPermissions() != null)
group.setPermissions(dto.getPermissions());
return ResponseEntity.ok(groupRepository.save(group));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ResponseEntity<Void> deleteGroup(@PathVariable UUID id) { public ResponseEntity<Void> deleteGroup(@PathVariable UUID id) {
groupRepository.deleteById(id); userService.deleteGroup(id);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@@ -62,5 +48,4 @@ public class GroupController {
public ResponseEntity<List<UserGroup>> getAllGroups() { public ResponseEntity<List<UserGroup>> getAllGroups() {
return ResponseEntity.ok(userService.getAllGroups()); return ResponseEntity.ok(userService.getAllGroups());
} }
} }

View File

@@ -1,47 +1,41 @@
package org.raddatz.familienarchiv.controller; package org.raddatz.familienarchiv.controller;
import lombok.RequiredArgsConstructor; import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.raddatz.familienarchiv.model.Document; import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.repository.DocumentRepository; import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.service.PersonService; import org.raddatz.familienarchiv.service.PersonService;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.List; import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/persons") @RequestMapping("/api/persons")
@RequiredArgsConstructor @RequiredArgsConstructor
public class PersonController { public class PersonController {
private final PersonRepository personRepository;
private final DocumentRepository documentRepository;
private final PersonService personService; private final PersonService personService;
private final DocumentService documentService;
@GetMapping @GetMapping
public ResponseEntity<List<Person>> getPersons(@RequestParam(required = false) String q) { public ResponseEntity<List<Person>> getPersons(@RequestParam(required = false) String q) {
if (q != null && !q.isBlank()) { return ResponseEntity.ok(personService.findAll(q));
return ResponseEntity.ok(personRepository.searchByName(q));
}
return ResponseEntity.ok(personRepository.findAllByOrderByLastNameAscFirstNameAsc());
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public Person getPerson(@PathVariable UUID id) { public Person getPerson(@PathVariable UUID id) {
return personRepository.findById(id) return personService.getById(id);
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Person nicht gefunden"));
} }
@GetMapping("/{id}/documents") @GetMapping("/{id}/documents")
public List<Document> getPersonDocuments(@PathVariable UUID id) { public List<Document> getPersonDocuments(@PathVariable UUID id) {
return documentRepository.findBySenderId(id); return documentService.getDocumentsBySender(id);
} }
@PostMapping @PostMapping

View File

@@ -4,14 +4,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.raddatz.familienarchiv.model.Document;
import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.Permission;
import org.raddatz.familienarchiv.security.RequirePermission; import org.raddatz.familienarchiv.security.RequirePermission;
import org.raddatz.familienarchiv.service.DocumentService;
import org.raddatz.familienarchiv.service.TagService;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -21,46 +19,31 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RestController @RestController
@RequestMapping("/api/tags") @RequestMapping("/api/tags")
@RequiredArgsConstructor @RequiredArgsConstructor
public class TagController { public class TagController {
private final TagRepository tagRepository;
private final DocumentRepository documentRepository;
// Rename Tag private final TagService tagService;
private final DocumentService documentService;
@PutMapping("/{id}") @PutMapping("/{id}")
@RequirePermission(Permission.ADMIN_TAG) @RequirePermission(Permission.ADMIN_TAG)
public ResponseEntity<Tag> updateTag(@PathVariable UUID id, @RequestBody Map<String, String> payload) { public ResponseEntity<Tag> updateTag(@PathVariable UUID id, @RequestBody Map<String, String> payload) {
Tag tag = tagRepository.findById(id).orElseThrow(); return ResponseEntity.ok(tagService.update(id, payload.get("name")));
tag.setName(payload.get("name"));
return ResponseEntity.ok(tagRepository.save(tag));
} }
// Delete Tag
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@RequirePermission(Permission.ADMIN_TAG) @RequirePermission(Permission.ADMIN_TAG)
@Transactional
public ResponseEntity<Void> deleteTag(@PathVariable UUID id) { public ResponseEntity<Void> deleteTag(@PathVariable UUID id) {
Tag tag = tagRepository.findById(id).orElseThrow(); documentService.deleteTagCascading(id);
// Remove tag from all documents first to prevent FK constraint errors
List<Document> documents = documentRepository.findByTags_Id(id);
for (Document doc : documents) {
doc.getTags().remove(tag);
documentRepository.save(doc);
}
tagRepository.delete(tag);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping @GetMapping
public List<Tag> searchTags(@RequestParam(defaultValue = "") String query) { public List<Tag> searchTags(@RequestParam(defaultValue = "") String query) {
return tagRepository.findByNameContainingIgnoreCase(query); return tagService.search(query);
} }
} }

View File

@@ -9,8 +9,6 @@ import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository; import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.DomainException;
@@ -36,9 +34,9 @@ import static org.raddatz.familienarchiv.repository.DocumentSpecifications.*;
public class DocumentService { public class DocumentService {
private final DocumentRepository documentRepository; private final DocumentRepository documentRepository;
private final PersonRepository personRepository; private final PersonService personService;
private final FileService fileService; private final FileService fileService;
private final TagRepository tagRepository; private final TagService tagService;
/** /**
* Lädt eine Datei hoch. * Lädt eine Datei hoch.
@@ -109,12 +107,12 @@ public class DocumentService {
// Sender // Sender
if (dto.getSenderId() != null) { if (dto.getSenderId() != null) {
doc.setSender(personRepository.findById(dto.getSenderId()).orElse(null)); doc.setSender(personService.getById(dto.getSenderId()));
} }
// Empfänger // Empfänger
if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) { if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) {
doc.setReceivers(new HashSet<>(personRepository.findAllById(dto.getReceiverIds()))); doc.setReceivers(new HashSet<>(personService.getAllById(dto.getReceiverIds())));
} }
// Datei // Datei
@@ -153,17 +151,14 @@ public class DocumentService {
// 2. Sender verknüpfen // 2. Sender verknüpfen
if (dto.getSenderId() != null) { if (dto.getSenderId() != null) {
Person sender = personRepository.findById(dto.getSenderId()).orElse(null); doc.setSender(personService.getById(dto.getSenderId()));
doc.setSender(sender);
} else { } else {
doc.setSender(null); doc.setSender(null);
} }
// 3. Empfänger verknüpfen // 3. Empfänger verknüpfen
if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) { if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) {
List<Person> receivers = personRepository.findAllById(dto.getReceiverIds()); doc.setReceivers(new HashSet<>(personService.getAllById(dto.getReceiverIds())));
doc.setReceivers(new HashSet<>(receivers));
} else { } else {
doc.getReceivers().clear(); // Alle entfernen doc.getReceivers().clear(); // Alle entfernen
} }
@@ -195,11 +190,7 @@ public class DocumentService {
if (cleanName.isEmpty()) if (cleanName.isEmpty())
continue; continue;
// Find existing or Create new newTags.add(tagService.findOrCreate(cleanName));
Tag tag = tagRepository.findByNameIgnoreCase(cleanName)
.orElseGet(() -> tagRepository.save(Tag.builder().name(cleanName).build()));
newTags.add(tag);
} }
doc.setTags(newTags); doc.setTags(newTags);
@@ -253,4 +244,28 @@ public class DocumentService {
return documentRepository.findAll(conversation, Sort.by(Sort.Direction.ASC, "documentDate")); return documentRepository.findAll(conversation, Sort.by(Sort.Direction.ASC, "documentDate"));
} }
public Document getDocumentById(UUID id) {
return documentRepository.findById(id)
.orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id));
}
public List<Document> getDocumentsBySender(UUID senderId) {
return documentRepository.findBySenderId(senderId);
}
public List<Document> getConversationFiltered(UUID senderId, UUID receiverId, LocalDate from, LocalDate to, Sort sort) {
LocalDate dateFrom = (from != null) ? from : LocalDate.parse("0000-01-01");
LocalDate dateTo = (to != null) ? to : LocalDate.now();
return documentRepository.findConversation(senderId, receiverId, dateFrom, dateTo, sort);
}
@Transactional
public void deleteTagCascading(UUID tagId) {
documentRepository.findByTags_Id(tagId).forEach(doc -> {
doc.getTags().removeIf(t -> t.getId().equals(tagId));
documentRepository.save(doc);
});
tagService.delete(tagId);
}
} }

View File

@@ -10,8 +10,6 @@ import org.raddatz.familienarchiv.model.DocumentStatus;
import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.model.Tag; import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.DocumentRepository; import org.raddatz.familienarchiv.repository.DocumentRepository;
import org.raddatz.familienarchiv.repository.PersonRepository;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -57,8 +55,8 @@ public class MassImportService {
} }
private final DocumentRepository documentRepository; private final DocumentRepository documentRepository;
private final PersonRepository personRepository; private final PersonService personService;
private final TagRepository tagRepository; private final TagService tagService;
private final S3Client s3Client; private final S3Client s3Client;
@Value("${app.s3.bucket}") @Value("${app.s3.bucket}")
@@ -307,8 +305,7 @@ public class MassImportService {
Tag tag = null; Tag tag = null;
if (!tagRaw.isBlank()) { if (!tagRaw.isBlank()) {
tag = tagRepository.findByNameIgnoreCase(tagRaw) tag = tagService.findOrCreate(tagRaw);
.orElseGet(() -> tagRepository.save(Tag.builder().name(tagRaw).build()));
} }
Document doc = existing.orElse(Document.builder() Document doc = existing.orElse(Document.builder()
@@ -362,15 +359,7 @@ public class MassImportService {
} }
private Person findOrCreatePerson(String rawName) { private Person findOrCreatePerson(String rawName) {
String alias = rawName.trim(); return personService.findOrCreateByAlias(rawName);
return personRepository.findByAliasIgnoreCase(alias).orElseGet(() -> {
PersonNameParser.SplitName split = PersonNameParser.split(alias);
return personRepository.save(Person.builder()
.alias(alias)
.firstName(split.firstName())
.lastName(split.lastName())
.build());
});
} }
private Optional<File> findFileRecursive(String filename) { private Optional<File> findFileRecursive(String filename) {

View File

@@ -1,6 +1,8 @@
package org.raddatz.familienarchiv.service; package org.raddatz.familienarchiv.service;
import lombok.RequiredArgsConstructor; import java.util.List;
import java.util.UUID;
import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.repository.PersonRepository; import org.raddatz.familienarchiv.repository.PersonRepository;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -8,7 +10,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.UUID; import lombok.RequiredArgsConstructor;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -16,6 +18,35 @@ public class PersonService {
private final PersonRepository personRepository; private final PersonRepository personRepository;
public List<Person> findAll(String q) {
if (q != null && !q.isBlank()) {
return personRepository.searchByName(q);
}
return personRepository.findAllByOrderByLastNameAscFirstNameAsc();
}
public Person getById(UUID id) {
return personRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Person nicht gefunden"));
}
public List<Person> getAllById(List<UUID> ids) {
return personRepository.findAllById(ids);
}
@Transactional
public Person findOrCreateByAlias(String rawName) {
String alias = rawName.trim();
return personRepository.findByAliasIgnoreCase(alias).orElseGet(() -> {
PersonNameParser.SplitName split = PersonNameParser.split(alias);
return personRepository.save(Person.builder()
.alias(alias)
.firstName(split.firstName())
.lastName(split.lastName())
.build());
});
}
@Transactional @Transactional
public Person createPerson(String firstName, String lastName, String alias) { public Person createPerson(String firstName, String lastName, String alias) {
Person person = Person.builder() Person person = Person.builder()

View File

@@ -0,0 +1,47 @@
package org.raddatz.familienarchiv.service;
import java.util.List;
import java.util.UUID;
import org.raddatz.familienarchiv.model.Tag;
import org.raddatz.familienarchiv.repository.TagRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class TagService {
private final TagRepository tagRepository;
public List<Tag> search(String query) {
return tagRepository.findByNameContainingIgnoreCase(query);
}
public Tag getById(UUID id) {
return tagRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Tag nicht gefunden"));
}
public Tag findOrCreate(String name) {
String cleanName = name.trim();
return tagRepository.findByNameIgnoreCase(cleanName)
.orElseGet(() -> tagRepository.save(Tag.builder().name(cleanName).build()));
}
@Transactional
public Tag update(UUID id, String newName) {
Tag tag = getById(id);
tag.setName(newName);
return tagRepository.save(tag);
}
@Transactional
public void delete(UUID id) {
tagRepository.delete(getById(id));
}
}

View File

@@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.dto.CreateUserRequest; import org.raddatz.familienarchiv.dto.CreateUserRequest;
import org.raddatz.familienarchiv.dto.GroupDTO;
import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.AppUser;
@@ -81,4 +82,30 @@ public AppUser createUserOrUpdate(CreateUserRequest request) {
public List<UserGroup> getAllGroups() { public List<UserGroup> getAllGroups() {
return groupRepository.findAll(); return groupRepository.findAll();
} }
public UserGroup getGroupById(UUID id) {
return groupRepository.findById(id)
.orElseThrow(() -> DomainException.notFound(ErrorCode.INTERNAL_ERROR, "Group not found: " + id));
}
@Transactional
public UserGroup createGroup(GroupDTO dto) {
UserGroup group = new UserGroup();
group.setName(dto.getName());
group.setPermissions(dto.getPermissions());
return groupRepository.save(group);
}
@Transactional
public UserGroup updateGroup(UUID id, GroupDTO dto) {
UserGroup group = getGroupById(id);
if (dto.getName() != null) group.setName(dto.getName());
if (dto.getPermissions() != null) group.setPermissions(dto.getPermissions());
return groupRepository.save(group);
}
@Transactional
public void deleteGroup(UUID id) {
groupRepository.deleteById(id);
}
} }