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 java.util.UUID;
import static org.codiki.domain.category.model.builder.CategoryBuilder.aCategory; 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.CategoryEditionException;
import org.codiki.domain.category.exception.CategoryNotFoundException; import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.category.model.Category; import org.codiki.domain.category.model.Category;
import org.codiki.domain.category.model.builder.CategoryBuilder;
import org.codiki.domain.category.port.CategoryPort; import org.codiki.domain.category.port.CategoryPort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -33,7 +34,11 @@ public class CategoryUseCases {
List<Category> subCategories = emptyList(); List<Category> subCategories = emptyList();
if (!isNull(subCategoryIds)) { if (!isNull(subCategoryIds)) {
subCategories = categoryPort.findAllByIds(subCategoryIds); try {
subCategories = categoryPort.findAllByIds(subCategoryIds);
} catch (CategoryNotFoundException exception) {
throw new CategoryEditionException(exception);
}
} }
Category newCategory = aCategory() Category newCategory = aCategory()
@@ -48,19 +53,44 @@ public class CategoryUseCases {
} }
public Category updateCategory(UUID categoryId, String name, List<UUID> subCategoryIds) { 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"); throw new CategoryEditionException("no any field is filled");
} }
Category categoryToUpdate = categoryPort.findById(categoryId) Category categoryToUpdate = categoryPort.findById(categoryId)
.orElseThrow(() -> new CategoryNotFoundException(categoryId)); .orElseThrow(() -> new CategoryNotFoundException(categoryId));
Category updatedCategory = aCategory() CategoryBuilder categoryBuilder = aCategory()
.basedOn(categoryToUpdate) .basedOn(categoryToUpdate);
.build();
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); categoryPort.save(updatedCategory);
return 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); void save(Category category);
List<Category> findAllByIds(List<UUID> subCategoryIds); 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 java.util.UUID;
import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import org.codiki.application.category.CategoryUseCases; import org.codiki.application.category.CategoryUseCases;
import org.codiki.domain.category.model.Category; import org.codiki.domain.category.model.Category;
import org.codiki.exposition.category.model.CategoryDto; import org.codiki.exposition.category.model.CategoryDto;
import org.codiki.exposition.category.model.CategoryEditionRequest; 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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
@@ -43,4 +45,10 @@ public class CategoryController {
); );
return new CategoryDto(createdCategory); 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.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.UNAUTHORIZED; 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.CategoryEditionException;
import org.codiki.domain.category.exception.CategoryNotFoundException; import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.exception.LoginFailureException; import org.codiki.domain.exception.LoginFailureException;
@@ -73,4 +74,10 @@ public class GlobalControllerExceptionHandler {
public void handleCategoryEditionException() { public void handleCategoryEditionException() {
// Do nothing. // 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; 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.GET;
import static org.springframework.http.HttpMethod.OPTIONS; import static org.springframework.http.HttpMethod.OPTIONS;
import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -52,6 +54,14 @@ public class SecurityConfiguration {
POST, POST,
"/api/categories" "/api/categories"
).hasRole("ADMIN") ).hasRole("ADMIN")
.requestMatchers(
PUT,
"/api/categories/{categoryId}"
).hasRole("ADMIN")
.requestMatchers(
DELETE,
"/api/categories/{categoryId}"
).hasRole("ADMIN")
.requestMatchers(OPTIONS).permitAll() .requestMatchers(OPTIONS).permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
); );

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.category.model.Category; import org.codiki.domain.category.model.Category;
import org.codiki.domain.category.port.CategoryPort; import org.codiki.domain.category.port.CategoryPort;
import org.codiki.infrastructure.category.model.CategoryEntity; import org.codiki.infrastructure.category.model.CategoryEntity;
@@ -31,10 +32,34 @@ public class CategoryJpaAdapter implements CategoryPort {
} }
@Override @Override
public List<Category> findAllByIds(final List<UUID> subCategoryIds) { public List<Category> findAllByIds(List<UUID> categoryIds) {
return categoryRepository.findAllById(subCategoryIds) final List<Category> categories = categoryRepository.findAllById(categoryIds)
.stream() .stream()
.map(CategoryEntity::toDomain) .map(CategoryEntity::toDomain)
.toList(); .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.codiki.infrastructure.category.model.CategoryEntity;
import org.springframework.data.jpa.repository.JpaRepository; 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> { 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 { vars {
url: http://localhost:8080 url: http://localhost:8080
publicationId: fce1de27-11c6-4deb-a248-b63288c00037 publicationId: fce1de27-11c6-4deb-a248-b63288c00037
bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNWExM2RjNy0wMjlkLTRlYWItYTYzZC1jMWU5NmY5MDI0MWQiLCJleHAiOjE3MTAyNTE0MzV9.0-KmVfwoyJ1JDZs-f2paEZVAljCPVkcEi33bYra4hoVSvECFsdc0CFlJKpWEeEswIv4jSsnEzs7yFW_XM9WWAA bearerToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxNWExM2RjNy0wMjlkLTRlYWItYTYzZC1jMWU5NmY5MDI0MWQiLCJleHAiOjE3MTAyNjMyOTh9.j-PRoIYVjPcpaE92u8sEUsZib1mGTrZmfd96ZgnGgskUqhnoUtCHwQbmbvHTdkr2XdypSU3Hq9dndwxU4ElmWA
categoryId: 25742844-f0c2-454a-a4d1-71ad3bda28df categoryId: 872abbb4-a287-4519-8eeb-c43d567d89c8
} }