feat(backend): add heroImageUrl and tags to RecipeSummaryResponse
GET /v1/recipes was returning RecipeSummaryResponse with no tags and only heroImagePreview. The planner frontend needs protein tags to pick gradient backgrounds for tiles without a hero image. - Replace JPQL constructor projection with entity query + LEFT JOIN FETCH tags - Map Recipe entity to RecipeSummaryResponse in service (includes tags + heroImageUrl) - Drop heroImagePreview in favour of heroImageUrl on the summary DTO Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
package com.recipeapp.recipe;
|
package com.recipeapp.recipe;
|
||||||
|
|
||||||
import com.recipeapp.recipe.dto.RecipeSummaryResponse;
|
|
||||||
import com.recipeapp.recipe.entity.Recipe;
|
import com.recipeapp.recipe.entity.Recipe;
|
||||||
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.jpa.repository.Query;
|
||||||
@@ -17,9 +16,8 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
|
|||||||
List<Recipe> findByHouseholdIdAndDeletedAtIsNull(UUID householdId);
|
List<Recipe> findByHouseholdIdAndDeletedAtIsNull(UUID householdId);
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT new com.recipeapp.recipe.dto.RecipeSummaryResponse(
|
SELECT r FROM Recipe r
|
||||||
r.id, r.name, r.serves, r.cookTimeMin, r.effort, r.heroImagePreview)
|
LEFT JOIN FETCH r.tags
|
||||||
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), '%')))
|
||||||
@@ -27,7 +25,7 @@ public interface RecipeRepository extends JpaRepository<Recipe, UUID> {
|
|||||||
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
|
||||||
""")
|
""")
|
||||||
List<RecipeSummaryResponse> findFiltered(
|
List<Recipe> findFiltered(
|
||||||
@Param("householdId") UUID householdId,
|
@Param("householdId") UUID householdId,
|
||||||
@Param("search") String search,
|
@Param("search") String search,
|
||||||
@Param("effort") String effort,
|
@Param("effort") String effort,
|
||||||
|
|||||||
@@ -42,7 +42,15 @@ public class RecipeService {
|
|||||||
@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,
|
||||||
Integer cookTimeMaxMin, String sort, int limit, int offset) {
|
Integer cookTimeMaxMin, String sort, int limit, int offset) {
|
||||||
return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset);
|
return recipeRepository.findFiltered(householdId, search, effort, cookTimeMaxMin, sort, limit, offset)
|
||||||
|
.stream()
|
||||||
|
.map(r -> new RecipeSummaryResponse(
|
||||||
|
r.getId(), r.getName(), r.getServes(), r.getCookTimeMin(), r.getEffort(),
|
||||||
|
r.getHeroImageUrl(),
|
||||||
|
r.getTags().stream()
|
||||||
|
.map(t -> new TagResponse(t.getId(), t.getName(), t.getTagType()))
|
||||||
|
.toList()))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.recipeapp.recipe.dto;
|
package com.recipeapp.recipe.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record RecipeSummaryResponse(
|
public record RecipeSummaryResponse(
|
||||||
@@ -8,5 +9,6 @@ public record RecipeSummaryResponse(
|
|||||||
short serves,
|
short serves,
|
||||||
short cookTimeMin,
|
short cookTimeMin,
|
||||||
String effort,
|
String effort,
|
||||||
String heroImagePreview
|
String heroImageUrl,
|
||||||
|
List<TagResponse> tags
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ class RecipeControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listRecipesShouldReturn200WithPagination() throws Exception {
|
void listRecipesShouldReturn200WithPagination() throws Exception {
|
||||||
|
var tag = new TagResponse(UUID.randomUUID(), "Rind", "protein");
|
||||||
var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese",
|
var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese",
|
||||||
(short) 4, (short) 45, "medium", null);
|
(short) 4, (short) 45, "medium", "https://example.com/img.jpg", List.of(tag));
|
||||||
|
|
||||||
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(),
|
when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(),
|
||||||
@@ -62,6 +63,9 @@ class RecipeControllerTest {
|
|||||||
.param("offset", "0"))
|
.param("offset", "0"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.data[0].name").value("Spaghetti Bolognese"))
|
.andExpect(jsonPath("$.data[0].name").value("Spaghetti Bolognese"))
|
||||||
|
.andExpect(jsonPath("$.data[0].heroImageUrl").value("https://example.com/img.jpg"))
|
||||||
|
.andExpect(jsonPath("$.data[0].tags[0].name").value("Rind"))
|
||||||
|
.andExpect(jsonPath("$.data[0].tags[0].tagType").value("protein"))
|
||||||
.andExpect(jsonPath("$.meta.pagination.total").value(1))
|
.andExpect(jsonPath("$.meta.pagination.total").value(1))
|
||||||
.andExpect(jsonPath("$.meta.pagination.hasMore").value(false));
|
.andExpect(jsonPath("$.meta.pagination.hasMore").value(false));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user