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:
@@ -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<RecipeSummaryResponse> 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);
|
||||
|
||||
@@ -18,13 +18,12 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
|
||||
|
||||
@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<Recipe, UUID> {
|
||||
@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<Recipe, UUID> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<RecipeSummaryResponse> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ public record RecipeDetailResponse(
|
||||
short serves,
|
||||
short cookTimeMin,
|
||||
String effort,
|
||||
boolean isChildFriendly,
|
||||
String heroImageUrl,
|
||||
List<IngredientItem> ingredients,
|
||||
List<StepItem> steps,
|
||||
|
||||
@@ -8,6 +8,5 @@ public record RecipeSummaryResponse(
|
||||
short serves,
|
||||
short cookTimeMin,
|
||||
String effort,
|
||||
boolean isChildFriendly,
|
||||
String heroImageUrl
|
||||
String heroImagePreview
|
||||
) {}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE recipe DROP COLUMN is_child_friendly;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.")),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
5
frontend/src/lib/api/schema.d.ts
vendored
5
frontend/src/lib/api/schema.d.ts
vendored
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user