Fix and polish category CRUD.

This commit is contained in:
Florian THIERRY
2024-03-12 17:55:20 +01:00
parent 94180e8efc
commit a3295636b4
9 changed files with 118 additions and 11 deletions

View File

@@ -7,10 +7,11 @@ import java.util.Optional;
import java.util.UUID;
import static org.codiki.domain.category.model.builder.CategoryBuilder.aCategory;
import static org.springframework.util.CollectionUtils.isEmpty;
import org.codiki.domain.category.exception.CategoryDeletionException;
import org.codiki.domain.category.exception.CategoryEditionException;
import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.category.model.Category;
import org.codiki.domain.category.model.builder.CategoryBuilder;
import org.codiki.domain.category.port.CategoryPort;
import org.springframework.stereotype.Service;
@@ -33,7 +34,11 @@ public class CategoryUseCases {
List<Category> subCategories = emptyList();
if (!isNull(subCategoryIds)) {
subCategories = categoryPort.findAllByIds(subCategoryIds);
try {
subCategories = categoryPort.findAllByIds(subCategoryIds);
} catch (CategoryNotFoundException exception) {
throw new CategoryEditionException(exception);
}
}
Category newCategory = aCategory()
@@ -48,19 +53,44 @@ public class CategoryUseCases {
}
public Category updateCategory(UUID categoryId, String name, List<UUID> subCategoryIds) {
if (isNull(name) && isEmpty(subCategoryIds)) {
if (isNull(name) && isNull(subCategoryIds)) {
throw new CategoryEditionException("no any field is filled");
}
Category categoryToUpdate = categoryPort.findById(categoryId)
.orElseThrow(() -> new CategoryNotFoundException(categoryId));
Category updatedCategory = aCategory()
.basedOn(categoryToUpdate)
.build();
CategoryBuilder categoryBuilder = aCategory()
.basedOn(categoryToUpdate);
if (!isNull(name)) {
categoryBuilder.withName(name);
}
if (!isNull(subCategoryIds)) {
List<Category> subCategories;
try {
subCategories = categoryPort.findAllByIds(subCategoryIds);
} catch (CategoryNotFoundException exception) {
throw new CategoryEditionException(exception);
}
categoryBuilder.withSubCategories(subCategories);
}
Category updatedCategory = categoryBuilder.build();
categoryPort.save(updatedCategory);
return updatedCategory;
}
public void deleteCategory(UUID categoryId) {
if (!categoryPort.existsById(categoryId)) {
throw new CategoryNotFoundException(categoryId);
}
if (categoryPort.existsAnyAssociatedPublication(categoryId)) {
throw new CategoryDeletionException(categoryId, "some publications are associated to the category");
}
categoryPort.deleteById(categoryId);
}
}

View File

@@ -0,0 +1,11 @@
package org.codiki.domain.category.exception;
import java.util.UUID;
import org.codiki.domain.exception.FunctionnalException;
public class CategoryDeletionException extends FunctionnalException {
public CategoryDeletionException(UUID categoryId, String cause) {
super(String.format("Impossible to delete category with id %s. Cause: %s.", categoryId, cause));
}
}

View File

@@ -12,4 +12,10 @@ public interface CategoryPort {
void save(Category category);
List<Category> findAllByIds(List<UUID> subCategoryIds);
boolean existsAnyAssociatedPublication(UUID categoryId);
void deleteById(UUID categoryId);
boolean existsById(UUID categoryId);
}

View File

@@ -3,10 +3,12 @@ package org.codiki.exposition.category;
import java.util.UUID;
import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import org.codiki.application.category.CategoryUseCases;
import org.codiki.domain.category.model.Category;
import org.codiki.exposition.category.model.CategoryDto;
import org.codiki.exposition.category.model.CategoryEditionRequest;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@@ -43,4 +45,10 @@ public class CategoryController {
);
return new CategoryDto(createdCategory);
}
@DeleteMapping("/{categoryId}")
@ResponseStatus(NO_CONTENT)
public void deleteCategory(@PathVariable("categoryId") UUID categoryId) {
categoryUseCases.deleteCategory(categoryId);
}
}

View File

@@ -4,6 +4,7 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import org.codiki.domain.category.exception.CategoryDeletionException;
import org.codiki.domain.category.exception.CategoryEditionException;
import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.exception.LoginFailureException;
@@ -73,4 +74,10 @@ public class GlobalControllerExceptionHandler {
public void handleCategoryEditionException() {
// Do nothing.
}
@ResponseStatus(BAD_REQUEST)
@ExceptionHandler(CategoryDeletionException.class)
public void handleCategoryDeletionException() {
// Do nothing.
}
}

View File

@@ -1,8 +1,10 @@
package org.codiki.exposition.configuration.security;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.OPTIONS;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -52,6 +54,14 @@ public class SecurityConfiguration {
POST,
"/api/categories"
).hasRole("ADMIN")
.requestMatchers(
PUT,
"/api/categories/{categoryId}"
).hasRole("ADMIN")
.requestMatchers(
DELETE,
"/api/categories/{categoryId}"
).hasRole("ADMIN")
.requestMatchers(OPTIONS).permitAll()
.anyRequest().authenticated()
);

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.category.model.Category;
import org.codiki.domain.category.port.CategoryPort;
import org.codiki.infrastructure.category.model.CategoryEntity;
@@ -31,10 +32,34 @@ public class CategoryJpaAdapter implements CategoryPort {
}
@Override
public List<Category> findAllByIds(final List<UUID> subCategoryIds) {
return categoryRepository.findAllById(subCategoryIds)
public List<Category> findAllByIds(List<UUID> categoryIds) {
final List<Category> categories = categoryRepository.findAllById(categoryIds)
.stream()
.map(CategoryEntity::toDomain)
.toList();
Optional<UUID> notFoundCategoryId = categories.stream()
.filter(category -> !categoryIds.contains(category.id()))
.findFirst()
.map(Category::id);
if (notFoundCategoryId.isPresent()) {
throw new CategoryNotFoundException(notFoundCategoryId.get());
}
return categories;
}
@Override
public boolean existsAnyAssociatedPublication(UUID categoryId) {
return categoryRepository.existsAnyAssociatedPublication(categoryId);
}
@Override
public void deleteById(UUID categoryId) {
categoryRepository.deleteById(categoryId);
}
@Override
public boolean existsById(UUID categoryId) {
return categoryRepository.existsById(categoryId);
}
}

View File

@@ -4,6 +4,16 @@ import java.util.UUID;
import org.codiki.infrastructure.category.model.CategoryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface CategoryRepository extends JpaRepository<CategoryEntity, UUID> {
@Query(value = """
SELECT (
SELECT COUNT(*)
FROM publication p
WHERE p.category_id = :categoryId
) > 0
""", nativeQuery = true)
boolean existsAnyAssociatedPublication(@Param("categoryId") UUID categoryId);
}

View File

@@ -1,6 +1,6 @@
vars {
url: http://localhost:8080
publicationId: fce1de27-11c6-4deb-a248-b63288c00037
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNWExM2RjNy0wMjlkLTRlYWItYTYzZC1jMWU5NmY5MDI0MWQiLCJleHAiOjE3MTAyNTE0MzV9.0-KmVfwoyJ1JDZs-f2paEZVAljCPVkcEi33bYra4hoVSvECFsdc0CFlJKpWEeEswIv4jSsnEzs7yFW_XM9WWAA
categoryId: 25742844-f0c2-454a-a4d1-71ad3bda28df
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNWExM2RjNy0wMjlkLTRlYWItYTYzZC1jMWU5NmY5MDI0MWQiLCJleHAiOjE3MTAyNjMyOTh9.j-PRoIYVjPcpaE92u8sEUsZib1mGTrZmfd96ZgnGgskUqhnoUtCHwQbmbvHTdkr2XdypSU3Hq9dndwxU4ElmWA
categoryId: 872abbb4-a287-4519-8eeb-c43d567d89c8
}