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 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 08:56:57 +02:00
parent 520dae5adf
commit 30ba53099c
14 changed files with 36 additions and 45 deletions

View File

@@ -29,7 +29,6 @@ public class RecipeController {
Principal principal, Principal principal,
@RequestParam(required = false) String search, @RequestParam(required = false) String search,
@RequestParam(required = false) String effort, @RequestParam(required = false) String effort,
@RequestParam(required = false) Boolean isChildFriendly,
@RequestParam(name = "cookTimeMin.lte", required = false) Integer cookTimeMaxMin, @RequestParam(name = "cookTimeMin.lte", required = false) Integer cookTimeMaxMin,
@RequestParam(required = false) String sort, @RequestParam(required = false) String sort,
@RequestParam(defaultValue = "20") int limit, @RequestParam(defaultValue = "20") int limit,
@@ -37,9 +36,9 @@ public class RecipeController {
UUID householdId = householdResolver.resolve(principal.getName()); UUID householdId = householdResolver.resolve(principal.getName());
List<RecipeSummaryResponse> recipes = recipeService.listRecipes( List<RecipeSummaryResponse> recipes = recipeService.listRecipes(
householdId, search, effort, isChildFriendly, cookTimeMaxMin, sort, limit, offset); householdId, search, effort, cookTimeMaxMin, sort, limit, offset);
long total = recipeService.countRecipes( 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 pagination = new ApiResponse.Pagination(total, limit, offset, offset + limit < total);
var meta = new ApiResponse.Meta(pagination); var meta = new ApiResponse.Meta(pagination);

View File

@@ -18,13 +18,12 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
@Query(""" @Query("""
SELECT new com.recipeapp.recipe.dto.RecipeSummaryResponse( 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 FROM Recipe r
WHERE r.household.id = :householdId WHERE r.household.id = :householdId
AND r.deletedAt IS NULL AND r.deletedAt IS NULL
AND (:search IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', CAST(:search AS string), '%'))) 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 (: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) AND (:cookTimeMaxMin IS NULL OR r.cookTimeMin <= :cookTimeMaxMin)
ORDER BY r.createdAt DESC ORDER BY r.createdAt DESC
""") """)
@@ -32,7 +31,6 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
@Param("householdId") UUID householdId, @Param("householdId") UUID householdId,
@Param("search") String search, @Param("search") String search,
@Param("effort") String effort, @Param("effort") String effort,
@Param("isChildFriendly") Boolean isChildFriendly,
@Param("cookTimeMaxMin") Integer cookTimeMaxMin, @Param("cookTimeMaxMin") Integer cookTimeMaxMin,
@Param("sort") String sort, @Param("sort") String sort,
@Param("limit") int limit, @Param("limit") int limit,
@@ -45,13 +43,11 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
AND r.deletedAt IS NULL AND r.deletedAt IS NULL
AND (:search IS NULL OR LOWER(r.name) LIKE LOWER(CONCAT('%', CAST(:search AS string), '%'))) 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 (: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) AND (:cookTimeMaxMin IS NULL OR r.cookTimeMin <= :cookTimeMaxMin)
""") """)
long countFiltered( long countFiltered(
@Param("householdId") UUID householdId, @Param("householdId") UUID householdId,
@Param("search") String search, @Param("search") String search,
@Param("effort") String effort, @Param("effort") String effort,
@Param("isChildFriendly") Boolean isChildFriendly,
@Param("cookTimeMaxMin") Integer cookTimeMaxMin); @Param("cookTimeMaxMin") Integer cookTimeMaxMin);
} }

View File

@@ -22,31 +22,31 @@ public class RecipeService {
private final TagRepository tagRepository; private final TagRepository tagRepository;
private final IngredientCategoryRepository ingredientCategoryRepository; private final IngredientCategoryRepository ingredientCategoryRepository;
private final HouseholdRepository householdRepository; private final HouseholdRepository householdRepository;
private final ImageCompressor imageCompressor;
public RecipeService(RecipeRepository recipeRepository, public RecipeService(RecipeRepository recipeRepository,
IngredientRepository ingredientRepository, IngredientRepository ingredientRepository,
TagRepository tagRepository, TagRepository tagRepository,
IngredientCategoryRepository ingredientCategoryRepository, IngredientCategoryRepository ingredientCategoryRepository,
HouseholdRepository householdRepository) { HouseholdRepository householdRepository,
ImageCompressor imageCompressor) {
this.recipeRepository = recipeRepository; this.recipeRepository = recipeRepository;
this.ingredientRepository = ingredientRepository; this.ingredientRepository = ingredientRepository;
this.tagRepository = tagRepository; this.tagRepository = tagRepository;
this.ingredientCategoryRepository = ingredientCategoryRepository; this.ingredientCategoryRepository = ingredientCategoryRepository;
this.householdRepository = householdRepository; this.householdRepository = householdRepository;
this.imageCompressor = imageCompressor;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<RecipeSummaryResponse> listRecipes(UUID householdId, String search, String effort, public List<RecipeSummaryResponse> listRecipes(UUID householdId, String search, String effort,
Boolean isChildFriendly, Integer cookTimeMaxMin, Integer cookTimeMaxMin, String sort, int limit, int offset) {
String sort, int limit, int offset) { return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset);
return recipeRepository.findFiltered(householdId, search, effort, isChildFriendly,
cookTimeMaxMin, sort, limit, offset);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
public long countRecipes(UUID householdId, String search, String effort, public long countRecipes(UUID householdId, String search, String effort, Integer cookTimeMaxMin) {
Boolean isChildFriendly, Integer cookTimeMaxMin) { return recipeRepository.countFiltered(householdId, search, effort, cookTimeMaxMin);
return recipeRepository.countFiltered(householdId, search, effort, isChildFriendly, cookTimeMaxMin);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -63,8 +63,9 @@ public class RecipeService {
Recipe recipe = new Recipe(household, request.name(), Recipe recipe = new Recipe(household, request.name(),
request.serves() != null ? request.serves().shortValue() : 0, request.serves() != null ? request.serves().shortValue() : 0,
request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0, request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0,
request.effort(), false); request.effort());
recipe.setHeroImageUrl(request.heroImageUrl()); recipe.setHeroImageUrl(request.heroImageUrl());
recipe.setHeroImagePreview(imageCompressor.compressToPreview(request.heroImageUrl()));
addIngredients(recipe, household, request.ingredients()); addIngredients(recipe, household, request.ingredients());
addSteps(recipe, request.steps()); addSteps(recipe, request.steps());
@@ -84,6 +85,7 @@ public class RecipeService {
recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0); recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0);
recipe.setEffort(request.effort()); recipe.setEffort(request.effort());
recipe.setHeroImageUrl(request.heroImageUrl()); recipe.setHeroImageUrl(request.heroImageUrl());
recipe.setHeroImagePreview(imageCompressor.compressToPreview(request.heroImageUrl()));
recipe.getIngredients().clear(); recipe.getIngredients().clear();
recipe.getSteps().clear(); recipe.getSteps().clear();
@@ -239,7 +241,7 @@ public class RecipeService {
return new RecipeDetailResponse( return new RecipeDetailResponse(
recipe.getId(), recipe.getName(), recipe.getServes(), recipe.getCookTimeMin(), recipe.getId(), recipe.getName(), recipe.getServes(), recipe.getCookTimeMin(),
recipe.getEffort(), recipe.isChildFriendly(), recipe.getHeroImageUrl(), recipe.getEffort(), recipe.getHeroImageUrl(),
ingredients, steps, tags); ingredients, steps, tags);
} }

View File

@@ -10,7 +10,6 @@ public record RecipeDetailResponse(
short serves, short serves,
short cookTimeMin, short cookTimeMin,
String effort, String effort,
boolean isChildFriendly,
String heroImageUrl, String heroImageUrl,
List<IngredientItem> ingredients, List<IngredientItem> ingredients,
List<StepItem> steps, List<StepItem> steps,

View File

@@ -8,6 +8,5 @@ public record RecipeSummaryResponse(
short serves, short serves,
short cookTimeMin, short cookTimeMin,
String effort, String effort,
boolean isChildFriendly, String heroImagePreview
String heroImageUrl
) {} ) {}

View File

@@ -33,12 +33,12 @@ public class Recipe {
@Column(nullable = false, length = 10) @Column(nullable = false, length = 10)
private String effort; private String effort;
@Column(name = "is_child_friendly", nullable = false)
private boolean isChildFriendly;
@Column(name = "hero_image_url", columnDefinition = "text") @Column(name = "hero_image_url", columnDefinition = "text")
private String heroImageUrl; private String heroImageUrl;
@Column(name = "hero_image_preview", columnDefinition = "text")
private String heroImagePreview;
@Column(name = "deleted_at") @Column(name = "deleted_at")
private Instant deletedAt; private Instant deletedAt;
@@ -64,14 +64,12 @@ public class Recipe {
protected Recipe() {} protected Recipe() {}
public Recipe(Household household, String name, short serves, short cookTimeMin, public Recipe(Household household, String name, short serves, short cookTimeMin, String effort) {
String effort, boolean isChildFriendly) {
this.household = household; this.household = household;
this.name = name; this.name = name;
this.serves = serves; this.serves = serves;
this.cookTimeMin = cookTimeMin; this.cookTimeMin = cookTimeMin;
this.effort = effort; this.effort = effort;
this.isChildFriendly = isChildFriendly;
} }
@PrePersist @PrePersist
@@ -95,10 +93,10 @@ public class Recipe {
public void setCookTimeMin(short cookTimeMin) { this.cookTimeMin = cookTimeMin; } public void setCookTimeMin(short cookTimeMin) { this.cookTimeMin = cookTimeMin; }
public String getEffort() { return effort; } public String getEffort() { return effort; }
public void setEffort(String effort) { this.effort = 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 String getHeroImageUrl() { return heroImageUrl; }
public void setHeroImageUrl(String heroImageUrl) { this.heroImageUrl = 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 Instant getDeletedAt() { return deletedAt; }
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
public Instant getCreatedAt() { return createdAt; } public Instant getCreatedAt() { return createdAt; }

View File

@@ -0,0 +1 @@
ALTER TABLE recipe DROP COLUMN is_child_friendly;

View File

@@ -55,7 +55,7 @@ class PlanningServiceTest {
} }
private Recipe testRecipe(Household household, String name) { 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()); setId(r, Recipe.class, UUID.randomUUID());
return r; return r;
} }

View File

@@ -69,7 +69,7 @@ class SuggestionsTest {
} }
private Recipe createRecipe(String name) { 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()); setId(r, Recipe.class, UUID.randomUUID());
return r; return r;
} }

View File

@@ -69,7 +69,7 @@ class VarietyScoreTest {
} }
private Recipe createRecipe(String name) { 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()); setId(r, Recipe.class, UUID.randomUUID());
return r; return r;
} }

View File

@@ -47,13 +47,13 @@ class RecipeControllerTest {
@Test @Test
void listRecipesShouldReturn200WithPagination() throws Exception { void listRecipesShouldReturn200WithPagination() throws Exception {
var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese", 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(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))) isNull(), eq(20), eq(0)))
.thenReturn(List.of(summary)); .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); .thenReturn(1L);
mockMvc.perform(get("/v1/recipes") mockMvc.perform(get("/v1/recipes")
@@ -69,17 +69,16 @@ class RecipeControllerTest {
@Test @Test
void listRecipesWithFiltersShouldPassParams() throws Exception { void listRecipesWithFiltersShouldPassParams() throws Exception {
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); 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))) eq(30), eq("-cookTimeMin"), eq(10), eq(5)))
.thenReturn(List.of()); .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); .thenReturn(0L);
mockMvc.perform(get("/v1/recipes") mockMvc.perform(get("/v1/recipes")
.principal(() -> "sarah@example.com") .principal(() -> "sarah@example.com")
.param("search", "pasta") .param("search", "pasta")
.param("effort", "easy") .param("effort", "easy")
.param("isChildFriendly", "true")
.param("cookTimeMin.lte", "30") .param("cookTimeMin.lte", "30")
.param("sort", "-cookTimeMin") .param("sort", "-cookTimeMin")
.param("limit", "10") .param("limit", "10")
@@ -175,7 +174,7 @@ class RecipeControllerTest {
private RecipeDetailResponse sampleDetail() { private RecipeDetailResponse sampleDetail() {
var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "pasta"); var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "pasta");
return new RecipeDetailResponse( 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( List.of(new RecipeDetailResponse.IngredientItem(
UUID.randomUUID(), "spaghetti", catRef, new BigDecimal("400"), "g", (short) 1)), UUID.randomUUID(), "spaghetti", catRef, new BigDecimal("400"), "g", (short) 1)),
List.of(new RecipeDetailResponse.StepItem((short) 1, "Boil water.")), List.of(new RecipeDetailResponse.StepItem((short) 1, "Boil water.")),

View File

@@ -27,6 +27,7 @@ class RecipeServiceTest {
@Mock private TagRepository tagRepository; @Mock private TagRepository tagRepository;
@Mock private IngredientCategoryRepository ingredientCategoryRepository; @Mock private IngredientCategoryRepository ingredientCategoryRepository;
@Mock private HouseholdRepository householdRepository; @Mock private HouseholdRepository householdRepository;
@Mock private ImageCompressor imageCompressor;
@InjectMocks private RecipeService recipeService; @InjectMocks private RecipeService recipeService;
@@ -43,7 +44,7 @@ class RecipeServiceTest {
} }
private Recipe testRecipe(Household household) { 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 { try {
var field = Recipe.class.getDeclaredField("id"); var field = Recipe.class.getDeclaredField("id");
field.setAccessible(true); field.setAccessible(true);

View File

@@ -60,7 +60,7 @@ class ShoppingServiceTest {
} }
private Recipe testRecipe(Household household, String name) { 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()); setId(r, Recipe.class, UUID.randomUUID());
return r; return r;
} }

View File

@@ -552,7 +552,6 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
cookTimeMin?: number; cookTimeMin?: number;
effort: string; effort: string;
isChildFriendly?: boolean;
heroImageUrl?: string; heroImageUrl?: string;
ingredients: components["schemas"]["IngredientEntry"][]; ingredients: components["schemas"]["IngredientEntry"][];
steps?: components["schemas"]["StepEntry"][]; steps?: components["schemas"]["StepEntry"][];
@@ -587,7 +586,6 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
cookTimeMin?: number; cookTimeMin?: number;
effort?: string; effort?: string;
isChildFriendly?: boolean;
heroImageUrl?: string; heroImageUrl?: string;
ingredients?: components["schemas"]["IngredientItem"][]; ingredients?: components["schemas"]["IngredientItem"][];
steps?: components["schemas"]["StepItem"][]; steps?: components["schemas"]["StepItem"][];
@@ -934,8 +932,7 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
cookTimeMin?: number; cookTimeMin?: number;
effort?: string; effort?: string;
isChildFriendly?: boolean; heroImagePreview?: string;
heroImageUrl?: string;
}; };
ApiResponseListAdminUserResponse: { ApiResponseListAdminUserResponse: {
status?: string; status?: string;