From 30ba53099cc3f6a9d205fa36d4bcd19340b27fa4 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 10 Apr 2026 08:56:57 +0200 Subject: [PATCH] refactor(recipes): drop is_child_friendly column and remove from all layers V025 migration drops the column. Removed from Recipe entity, RecipeDetailResponse, RecipeSummaryResponse, RecipeRepository JPQL, RecipeService, and RecipeController. Co-Authored-By: Claude Sonnet 4.6 --- .../recipeapp/recipe/RecipeController.java | 5 ++--- .../recipeapp/recipe/RecipeRepository.java | 6 +---- .../com/recipeapp/recipe/RecipeService.java | 22 ++++++++++--------- .../recipe/dto/RecipeDetailResponse.java | 1 - .../recipe/dto/RecipeSummaryResponse.java | 3 +-- .../com/recipeapp/recipe/entity/Recipe.java | 14 +++++------- .../V025__drop_is_child_friendly.sql | 1 + .../planning/PlanningServiceTest.java | 2 +- .../recipeapp/planning/SuggestionsTest.java | 2 +- .../recipeapp/planning/VarietyScoreTest.java | 2 +- .../recipe/RecipeControllerTest.java | 13 +++++------ .../recipeapp/recipe/RecipeServiceTest.java | 3 ++- .../shopping/ShoppingServiceTest.java | 2 +- frontend/src/lib/api/schema.d.ts | 5 +---- 14 files changed, 36 insertions(+), 45 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V025__drop_is_child_friendly.sql diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeController.java b/backend/src/main/java/com/recipeapp/recipe/RecipeController.java index 445cabe..a9c729a 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeController.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeController.java @@ -29,7 +29,6 @@ public class RecipeController { Principal principal, @RequestParam(required = false) String search, @RequestParam(required = false) String effort, - @RequestParam(required = false) Boolean isChildFriendly, @RequestParam(name = "cookTimeMin.lte", required = false) Integer cookTimeMaxMin, @RequestParam(required = false) String sort, @RequestParam(defaultValue = "20") int limit, @@ -37,9 +36,9 @@ public class RecipeController { UUID householdId = householdResolver.resolve(principal.getName()); List recipes = recipeService.listRecipes( - householdId, search, effort, isChildFriendly, cookTimeMaxMin, sort, limit, offset); + householdId, search, effort, cookTimeMaxMin, sort, limit, offset); long total = recipeService.countRecipes( - householdId, search, effort, isChildFriendly, cookTimeMaxMin); + householdId, search, effort, cookTimeMaxMin); var pagination = new ApiResponse.Pagination(total, limit, offset, offset + limit < total); var meta = new ApiResponse.Meta(pagination); diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java b/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java index f4b3c11..6b7a3d0 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeRepository.java @@ -18,13 +18,12 @@ public interface RecipeRepository extends JpaRepository { @Query(""" SELECT new com.recipeapp.recipe.dto.RecipeSummaryResponse( - r.id, r.name, r.serves, r.cookTimeMin, r.effort, r.isChildFriendly, r.heroImageUrl) + r.id, r.name, r.serves, r.cookTimeMin, r.effort, r.heroImagePreview) FROM Recipe r WHERE r.household.id = :householdId AND r.deletedAt IS NULL AND (:search IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', CAST(:search AS string), '%'))) AND (:effort IS NULL OR r.effort = CAST(:effort AS string)) - AND (:isChildFriendly IS NULL OR r.isChildFriendly = :isChildFriendly) AND (:cookTimeMaxMin IS NULL OR r.cookTimeMin <= :cookTimeMaxMin) ORDER BY r.createdAt DESC """) @@ -32,7 +31,6 @@ public interface RecipeRepository extends JpaRepository { @Param("householdId") UUID householdId, @Param("search") String search, @Param("effort") String effort, - @Param("isChildFriendly") Boolean isChildFriendly, @Param("cookTimeMaxMin") Integer cookTimeMaxMin, @Param("sort") String sort, @Param("limit") int limit, @@ -45,13 +43,11 @@ public interface RecipeRepository extends JpaRepository { AND r.deletedAt IS NULL AND (:search IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', CAST(:search AS string), '%'))) AND (:effort IS NULL OR r.effort = CAST(:effort AS string)) - AND (:isChildFriendly IS NULL OR r.isChildFriendly = :isChildFriendly) AND (:cookTimeMaxMin IS NULL OR r.cookTimeMin <= :cookTimeMaxMin) """) long countFiltered( @Param("householdId") UUID householdId, @Param("search") String search, @Param("effort") String effort, - @Param("isChildFriendly") Boolean isChildFriendly, @Param("cookTimeMaxMin") Integer cookTimeMaxMin); } diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java index de5471b..cfd36f2 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java @@ -22,31 +22,31 @@ public class RecipeService { private final TagRepository tagRepository; private final IngredientCategoryRepository ingredientCategoryRepository; private final HouseholdRepository householdRepository; + private final ImageCompressor imageCompressor; public RecipeService(RecipeRepository recipeRepository, IngredientRepository ingredientRepository, TagRepository tagRepository, IngredientCategoryRepository ingredientCategoryRepository, - HouseholdRepository householdRepository) { + HouseholdRepository householdRepository, + ImageCompressor imageCompressor) { this.recipeRepository = recipeRepository; this.ingredientRepository = ingredientRepository; this.tagRepository = tagRepository; this.ingredientCategoryRepository = ingredientCategoryRepository; this.householdRepository = householdRepository; + this.imageCompressor = imageCompressor; } @Transactional(readOnly = true) public List listRecipes(UUID householdId, String search, String effort, - Boolean isChildFriendly, Integer cookTimeMaxMin, - String sort, int limit, int offset) { - return recipeRepository.findFiltered(householdId, search, effort, isChildFriendly, - cookTimeMaxMin, sort, limit, offset); + Integer cookTimeMaxMin, String sort, int limit, int offset) { + return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset); } @Transactional(readOnly = true) - public long countRecipes(UUID householdId, String search, String effort, - Boolean isChildFriendly, Integer cookTimeMaxMin) { - return recipeRepository.countFiltered(householdId, search, effort, isChildFriendly, cookTimeMaxMin); + public long countRecipes(UUID householdId, String search, String effort, Integer cookTimeMaxMin) { + return recipeRepository.countFiltered(householdId, search, effort, cookTimeMaxMin); } @Transactional(readOnly = true) @@ -63,8 +63,9 @@ public class RecipeService { Recipe recipe = new Recipe(household, request.name(), request.serves() != null ? request.serves().shortValue() : 0, request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0, - request.effort(), false); + request.effort()); recipe.setHeroImageUrl(request.heroImageUrl()); + recipe.setHeroImagePreview(imageCompressor.compressToPreview(request.heroImageUrl())); addIngredients(recipe, household, request.ingredients()); addSteps(recipe, request.steps()); @@ -84,6 +85,7 @@ public class RecipeService { recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0); recipe.setEffort(request.effort()); recipe.setHeroImageUrl(request.heroImageUrl()); + recipe.setHeroImagePreview(imageCompressor.compressToPreview(request.heroImageUrl())); recipe.getIngredients().clear(); recipe.getSteps().clear(); @@ -239,7 +241,7 @@ public class RecipeService { return new RecipeDetailResponse( recipe.getId(), recipe.getName(), recipe.getServes(), recipe.getCookTimeMin(), - recipe.getEffort(), recipe.isChildFriendly(), recipe.getHeroImageUrl(), + recipe.getEffort(), recipe.getHeroImageUrl(), ingredients, steps, tags); } diff --git a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeDetailResponse.java b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeDetailResponse.java index 68bbc48..1639e8b 100644 --- a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeDetailResponse.java +++ b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeDetailResponse.java @@ -10,7 +10,6 @@ public record RecipeDetailResponse( short serves, short cookTimeMin, String effort, - boolean isChildFriendly, String heroImageUrl, List ingredients, List steps, diff --git a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java index 93ca6a7..c128982 100644 --- a/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java +++ b/backend/src/main/java/com/recipeapp/recipe/dto/RecipeSummaryResponse.java @@ -8,6 +8,5 @@ public record RecipeSummaryResponse( short serves, short cookTimeMin, String effort, - boolean isChildFriendly, - String heroImageUrl + String heroImagePreview ) {} diff --git a/backend/src/main/java/com/recipeapp/recipe/entity/Recipe.java b/backend/src/main/java/com/recipeapp/recipe/entity/Recipe.java index 508cd79..e6ce422 100644 --- a/backend/src/main/java/com/recipeapp/recipe/entity/Recipe.java +++ b/backend/src/main/java/com/recipeapp/recipe/entity/Recipe.java @@ -33,12 +33,12 @@ public class Recipe { @Column(nullable = false, length = 10) private String effort; - @Column(name = "is_child_friendly", nullable = false) - private boolean isChildFriendly; - @Column(name = "hero_image_url", columnDefinition = "text") private String heroImageUrl; + @Column(name = "hero_image_preview", columnDefinition = "text") + private String heroImagePreview; + @Column(name = "deleted_at") private Instant deletedAt; @@ -64,14 +64,12 @@ public class Recipe { protected Recipe() {} - public Recipe(Household household, String name, short serves, short cookTimeMin, - String effort, boolean isChildFriendly) { + public Recipe(Household household, String name, short serves, short cookTimeMin, String effort) { this.household = household; this.name = name; this.serves = serves; this.cookTimeMin = cookTimeMin; this.effort = effort; - this.isChildFriendly = isChildFriendly; } @PrePersist @@ -95,10 +93,10 @@ public class Recipe { public void setCookTimeMin(short cookTimeMin) { this.cookTimeMin = cookTimeMin; } public String getEffort() { return effort; } public void setEffort(String effort) { this.effort = effort; } - public boolean isChildFriendly() { return isChildFriendly; } - public void setChildFriendly(boolean childFriendly) { isChildFriendly = childFriendly; } public String getHeroImageUrl() { return heroImageUrl; } public void setHeroImageUrl(String heroImageUrl) { this.heroImageUrl = heroImageUrl; } + public String getHeroImagePreview() { return heroImagePreview; } + public void setHeroImagePreview(String heroImagePreview) { this.heroImagePreview = heroImagePreview; } public Instant getDeletedAt() { return deletedAt; } public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } public Instant getCreatedAt() { return createdAt; } diff --git a/backend/src/main/resources/db/migration/V025__drop_is_child_friendly.sql b/backend/src/main/resources/db/migration/V025__drop_is_child_friendly.sql new file mode 100644 index 0000000..9c4f1ba --- /dev/null +++ b/backend/src/main/resources/db/migration/V025__drop_is_child_friendly.sql @@ -0,0 +1 @@ +ALTER TABLE recipe DROP COLUMN is_child_friendly; diff --git a/backend/src/test/java/com/recipeapp/planning/PlanningServiceTest.java b/backend/src/test/java/com/recipeapp/planning/PlanningServiceTest.java index 6ab3f52..7387f2d 100644 --- a/backend/src/test/java/com/recipeapp/planning/PlanningServiceTest.java +++ b/backend/src/test/java/com/recipeapp/planning/PlanningServiceTest.java @@ -55,7 +55,7 @@ class PlanningServiceTest { } private Recipe testRecipe(Household household, String name) { - var r = new Recipe(household, name, (short) 4, (short) 45, "medium", true); + var r = new Recipe(household, name, (short) 4, (short) 45, "medium"); setId(r, Recipe.class, UUID.randomUUID()); return r; } diff --git a/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java b/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java index e86ff86..3add7f0 100644 --- a/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java +++ b/backend/src/test/java/com/recipeapp/planning/SuggestionsTest.java @@ -69,7 +69,7 @@ class SuggestionsTest { } private Recipe createRecipe(String name) { - var r = new Recipe(household, name, (short) 4, (short) 30, "medium", true); + var r = new Recipe(household, name, (short) 4, (short) 30, "medium"); setId(r, Recipe.class, UUID.randomUUID()); return r; } diff --git a/backend/src/test/java/com/recipeapp/planning/VarietyScoreTest.java b/backend/src/test/java/com/recipeapp/planning/VarietyScoreTest.java index dfcc8d9..5054fd5 100644 --- a/backend/src/test/java/com/recipeapp/planning/VarietyScoreTest.java +++ b/backend/src/test/java/com/recipeapp/planning/VarietyScoreTest.java @@ -69,7 +69,7 @@ class VarietyScoreTest { } private Recipe createRecipe(String name) { - var r = new Recipe(household, name, (short) 4, (short) 30, "medium", true); + var r = new Recipe(household, name, (short) 4, (short) 30, "medium"); setId(r, Recipe.class, UUID.randomUUID()); return r; } diff --git a/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java b/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java index 6f160da..ab3c505 100644 --- a/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java +++ b/backend/src/test/java/com/recipeapp/recipe/RecipeControllerTest.java @@ -47,13 +47,13 @@ class RecipeControllerTest { @Test void listRecipesShouldReturn200WithPagination() throws Exception { var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese", - (short) 4, (short) 45, "medium", true, null); + (short) 4, (short) 45, "medium", null); when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); - when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(), isNull(), + when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(), isNull(), eq(20), eq(0))) .thenReturn(List.of(summary)); - when(recipeService.countRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(), isNull())) + when(recipeService.countRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull())) .thenReturn(1L); mockMvc.perform(get("/v1/recipes") @@ -69,17 +69,16 @@ class RecipeControllerTest { @Test void listRecipesWithFiltersShouldPassParams() throws Exception { when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); - when(recipeService.listRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(true), + when(recipeService.listRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(30), eq("-cookTimeMin"), eq(10), eq(5))) .thenReturn(List.of()); - when(recipeService.countRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(true), eq(30))) + when(recipeService.countRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(30))) .thenReturn(0L); mockMvc.perform(get("/v1/recipes") .principal(() -> "sarah@example.com") .param("search", "pasta") .param("effort", "easy") - .param("isChildFriendly", "true") .param("cookTimeMin.lte", "30") .param("sort", "-cookTimeMin") .param("limit", "10") @@ -175,7 +174,7 @@ class RecipeControllerTest { private RecipeDetailResponse sampleDetail() { var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "pasta"); return new RecipeDetailResponse( - RECIPE_ID, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null, + RECIPE_ID, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", null, List.of(new RecipeDetailResponse.IngredientItem( UUID.randomUUID(), "spaghetti", catRef, new BigDecimal("400"), "g", (short) 1)), List.of(new RecipeDetailResponse.StepItem((short) 1, "Boil water.")), diff --git a/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java b/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java index 350e135..acf3ae4 100644 --- a/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java +++ b/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java @@ -27,6 +27,7 @@ class RecipeServiceTest { @Mock private TagRepository tagRepository; @Mock private IngredientCategoryRepository ingredientCategoryRepository; @Mock private HouseholdRepository householdRepository; + @Mock private ImageCompressor imageCompressor; @InjectMocks private RecipeService recipeService; @@ -43,7 +44,7 @@ class RecipeServiceTest { } private Recipe testRecipe(Household household) { - var r = new Recipe(household, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", true); + var r = new Recipe(household, "Spaghetti Bolognese", (short) 4, (short) 45, "medium"); try { var field = Recipe.class.getDeclaredField("id"); field.setAccessible(true); diff --git a/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java b/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java index d5d1f0c..6865191 100644 --- a/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java +++ b/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java @@ -60,7 +60,7 @@ class ShoppingServiceTest { } private Recipe testRecipe(Household household, String name) { - var r = new Recipe(household, name, (short) 4, (short) 45, "medium", true); + var r = new Recipe(household, name, (short) 4, (short) 45, "medium"); setId(r, Recipe.class, UUID.randomUUID()); return r; } diff --git a/frontend/src/lib/api/schema.d.ts b/frontend/src/lib/api/schema.d.ts index 9ee4ede..aadfed1 100644 --- a/frontend/src/lib/api/schema.d.ts +++ b/frontend/src/lib/api/schema.d.ts @@ -552,7 +552,6 @@ export interface components { /** Format: int32 */ cookTimeMin?: number; effort: string; - isChildFriendly?: boolean; heroImageUrl?: string; ingredients: components["schemas"]["IngredientEntry"][]; steps?: components["schemas"]["StepEntry"][]; @@ -587,7 +586,6 @@ export interface components { /** Format: int32 */ cookTimeMin?: number; effort?: string; - isChildFriendly?: boolean; heroImageUrl?: string; ingredients?: components["schemas"]["IngredientItem"][]; steps?: components["schemas"]["StepItem"][]; @@ -934,8 +932,7 @@ export interface components { /** Format: int32 */ cookTimeMin?: number; effort?: string; - isChildFriendly?: boolean; - heroImageUrl?: string; + heroImagePreview?: string; }; ApiResponseListAdminUserResponse: { status?: string;