diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java index 0e5e8dd..6d1461f 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeServiceTests.java @@ -1,5 +1,6 @@ package app.mealsmadeeasy.api.recipe; +import app.mealsmadeeasy.api.MinIOTestsExtension; import app.mealsmadeeasy.api.PostgresTestsExtension; import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.ImageException; @@ -36,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; // TODO: test prep/cooking/total times included @SpringBootTest @ExtendWith(PostgresTestsExtension.class) +@ExtendWith(MinIOTestsExtension.class) public class RecipeServiceTests { @Autowired @@ -107,16 +109,31 @@ public class RecipeServiceTests { public void whenCreate_allFieldsTransferredFromSpec() throws Exception { final User owner = this.seedUser(); final Image image = this.seedImage(owner); + + final List ingredients = List.of( + RecipeCreateSpec.IngredientCreateSpec.builder() + .amount("1T") + .name("Butter") + .notes("softened") + .build(), + RecipeCreateSpec.IngredientCreateSpec.builder() + .amount("2T") + .name("Shortening") + .notes("vegan") + .build() + ); final RecipeCreateSpec spec = RecipeCreateSpec.builder() .title("Recipe Title") .slug("recipe-slug") .preparationTime(15) .cookingTime(30) .totalTime(45) + .ingredients(ingredients) .rawText("# Hello Recipe") .isPublic(true) .mainImage(image) .build(); + final Recipe recipe = this.recipeService.create(owner, spec, false); assertThat(recipe.getId(), is(notNullValue())); assertThat(recipe.getTitle(), is("Recipe Title")); @@ -124,6 +141,8 @@ public class RecipeServiceTests { assertThat(recipe.getPreparationTime(), is(15)); assertThat(recipe.getCookingTime(), is(30)); assertThat(recipe.getTotalTime(), is(45)); + assertThat(recipe.getIngredients(), is(notNullValue())); + assertThat(recipe.getIngredients().size(), is(2)); assertThat(recipe.getRawText(), is("# Hello Recipe")); assertThat(recipe.getMainImage(), is(notNullValue())); assertThat(recipe.getMainImage().getId(), is(image.getId())); @@ -305,6 +324,61 @@ public class RecipeServiceTests { assertThat(viewableInfos, containsRecipes(r0, r1)); } + @Test + public void whenUpdate_allFieldsTransferred() throws Exception { + final User owner = this.seedUser(); + final Recipe base = this.createTestRecipe(owner); + + final List ingredients = List.of( + RecipeUpdateSpec.IngredientUpdateSpec.builder() + .amount("1T") + .name("Butter") + .notes("Softened") + .build(), + RecipeUpdateSpec.IngredientUpdateSpec.builder() + .amount("2T") + .name("Shortening") + .notes("Vegan") + .build() + ); + + final Image image = this.seedImage(owner); + final RecipeUpdateSpec.MainImageUpdateSpec mainImageSpec = RecipeUpdateSpec.MainImageUpdateSpec.builder() + .username(image.getOwner().getUsername()) + .filename(image.getUserFilename()) + .build(); + + final RecipeUpdateSpec spec = RecipeUpdateSpec.builder() + .title("New Title") + .preparationTime(20) + .cookingTime(40) + .totalTime(60) + .ingredients(ingredients) + .rawText("Updated text.") + .isPublic(true) + .mainImage(mainImageSpec) + .build(); + + final Recipe updated = this.recipeService.update( + base.getOwner().getUsername(), + base.getSlug(), + spec, + owner + ); + + assertThat(updated.getId(), is(base.getId())); + assertThat(updated.getTitle(), is("New Title")); + assertThat(updated.getSlug(), is(base.getSlug())); + assertThat(updated.getPreparationTime(), is(20)); + assertThat(updated.getCookingTime(), is(40)); + assertThat(updated.getTotalTime(), is(60)); + assertThat(updated.getIngredients(), is(notNullValue())); + assertThat(updated.getIngredients().size(), is(2)); + assertThat(updated.getIsPublic(), is(true)); + assertThat(updated.getMainImage(), is(notNullValue())); + assertThat(updated.getMainImage().getId(), is(image.getId())); + } + @Test public void updateRawText() { final User owner = this.seedUser(); diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipesControllerTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipesControllerTests.java index 51e573d..adaa95b 100644 --- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipesControllerTests.java +++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipesControllerTests.java @@ -1,5 +1,6 @@ package app.mealsmadeeasy.api.recipe; +import app.mealsmadeeasy.api.MinIOTestsExtension; import app.mealsmadeeasy.api.PostgresTestsExtension; import app.mealsmadeeasy.api.auth.AuthService; import app.mealsmadeeasy.api.auth.LoginDetails; @@ -25,6 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.io.InputStream; +import java.util.List; import java.util.UUID; import static org.hamcrest.Matchers.*; @@ -35,6 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringBootTest @AutoConfigureMockMvc @ExtendWith(PostgresTestsExtension.class) +@ExtendWith(MinIOTestsExtension.class) public class RecipesControllerTests { private static InputStream getHal9000() { @@ -222,13 +225,39 @@ public class RecipesControllerTests { public void updateRecipe() throws Exception { final User owner = this.seedUser(); final Recipe recipe = this.createTestRecipe(owner, false); - final String accessToken = this.getAccessToken(owner); - final String body = this.getUpdateBody(); + final Image image = this.createHal9000(owner); + + final RecipeUpdateSpec spec = RecipeUpdateSpec.builder() + .title("Updated Test Recipe") + .preparationTime(15) + .cookingTime(30) + .totalTime(45) + .ingredients(List.of( + RecipeUpdateSpec.IngredientUpdateSpec.builder() + .amount("1T") + .name("Butter") + .notes("Softened") + .build(), + RecipeUpdateSpec.IngredientUpdateSpec.builder() + .amount("2T") + .name("Shortening") + .notes("Vegan") + .build() + )) + .rawText("# Hello, Updated World!") + .isPublic(true) + .mainImage(RecipeUpdateSpec.MainImageUpdateSpec.builder() + .username(image.getOwner().getUsername()) + .filename(image.getUserFilename()) + .build()) + .build(); + final String updateBody = this.objectMapper.writeValueAsString(spec); + this.mockMvc.perform( post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) - .header("Authorization", "Bearer " + accessToken) + .header("Authorization", "Bearer " + this.getAccessToken(owner)) .contentType(MediaType.APPLICATION_JSON) - .content(body) + .content(updateBody) ) .andExpect(status().isOk()) .andExpect(jsonPath("$.recipe.id").value(recipe.getId())) @@ -236,6 +265,15 @@ public class RecipesControllerTests { .andExpect(jsonPath("$.recipe.preparationTime").value(15)) .andExpect(jsonPath("$.recipe.cookingTime").value(30)) .andExpect(jsonPath("$.recipe.totalTime").value(45)) + + .andExpect(jsonPath("$.recipe.ingredients").isArray()) + .andExpect(jsonPath("$.recipe.ingredients[0].amount").value("1T")) + .andExpect(jsonPath("$.recipe.ingredients[0].name").value("Butter")) + .andExpect(jsonPath("$.recipe.ingredients[0].notes").value("Softened")) + .andExpect(jsonPath("$.recipe.ingredients[1].amount").value("2T")) + .andExpect(jsonPath("$.recipe.ingredients[1].name").value("Shortening")) + .andExpect(jsonPath("$.recipe.ingredients[1].notes").value("Vegan")) + .andExpect(jsonPath("$.recipe.text").value("

Hello, Updated World!

")) .andExpect(jsonPath("$.recipe.rawText").value("# Hello, Updated World!")) .andExpect(jsonPath("$.recipe.owner.id").value(owner.getId())) @@ -243,7 +281,10 @@ public class RecipesControllerTests { .andExpect(jsonPath("$.recipe.starCount").value(0)) .andExpect(jsonPath("$.recipe.viewerCount").value(0)) .andExpect(jsonPath("$.recipe.public").value(true)) - .andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) + + .andExpect(jsonPath("$.recipe.mainImage.owner.username").value(owner.getUsername())) + .andExpect(jsonPath("$.recipe.mainImage.filename").value(image.getUserFilename())) + .andExpect(jsonPath("$.isStarred").value(false)) .andExpect(jsonPath("$.isOwner").value(true)); } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java b/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java index b4051df..3cb534b 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/Recipe.java @@ -4,14 +4,17 @@ import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.user.User; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.annotations.Type; import org.jetbrains.annotations.Nullable; import java.time.OffsetDateTime; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -21,20 +24,54 @@ import java.util.Set; @ToString public class Recipe { + @Getter + @Setter + @ToString + public static class Ingredient { + + @Nullable + private String amount; + + private String name; + + @Nullable + private String notes; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof Ingredient other) { + return Objects.equals(this.amount, other.amount) + && Objects.equals(this.name, other.name) + && Objects.equals(this.notes, other.notes); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(this.amount, this.name, this.notes); + } + + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(nullable = false, updatable = false) + @Basic(optional = false) + @Column(updatable = false) private Integer id; - @Column(nullable = false) + @Basic(optional = false) private OffsetDateTime created; private OffsetDateTime modified; - @Column(nullable = false, unique = true) + @Basic(optional = false) + @Column(unique = true) private String slug; - @Column(nullable = false) + @Basic(optional = false) private String title; @Nullable @@ -46,7 +83,13 @@ public class Recipe { @Nullable private Integer totalTime; - @Column(columnDefinition = "TEXT", nullable = false) + @Type(JsonBinaryType.class) + @Column(columnDefinition = "JSONB") + @Nullable + private List ingredients; + + @Basic(optional = false) + @Column(columnDefinition = "TEXT") private String rawText; @Column(columnDefinition = "TEXT") @@ -63,7 +106,7 @@ public class Recipe { @OneToMany(mappedBy = "recipe", orphanRemoval = true, cascade = CascadeType.ALL) private Set comments = new HashSet<>(); - @Column(nullable = false) + @Basic(optional = false) private Boolean isPublic = false; @ManyToMany diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java index bb771bb..10526e5 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java @@ -50,6 +50,20 @@ public class RecipeService { draft.setOwner(owner); draft.setSlug(spec.getSlug()); draft.setTitle(spec.getTitle()); + + if (spec.getIngredients() != null) { + draft.setIngredients(spec.getIngredients().stream() + .map(ingredientSpec -> { + final Recipe.Ingredient ingredient = new Recipe.Ingredient(); + ingredient.setAmount(ingredientSpec.amount()); + ingredient.setName(ingredientSpec.name()); + ingredient.setNotes(ingredientSpec.notes()); + return ingredient; + }) + .toList() + ); + } + draft.setRawText(spec.getRawText()); draft.setMainImage(spec.getMainImage()); draft.setIsPublic(spec.isPublic()); @@ -57,12 +71,14 @@ public class RecipeService { draft.setCookingTime(spec.getCookingTime()); draft.setTotalTime(spec.getTotalTime()); final Recipe saved = this.recipeRepository.save(draft); + if (queueSummaryJob) { this.jobService.create( RecipeSummaryJobHandler.JOB_KEY, new RecipeSummaryJobHandler.RecipeSummaryJobPayload(saved.getId()) ); } + return saved; } @@ -160,6 +176,19 @@ public class RecipeService { recipe.setTotalTime(spec.getTotalTime()); } + if (spec.getIngredients() != null) { + recipe.setIngredients(spec.getIngredients().stream() + .map(ingredientSpec -> { + final Recipe.Ingredient ingredient = new Recipe.Ingredient(); + ingredient.setAmount(ingredientSpec.amount()); + ingredient.setName(ingredientSpec.name()); + ingredient.setNotes(ingredientSpec.notes()); + return ingredient; + }) + .toList() + ); + } + if (spec.getRawText() != null) { recipe.setRawText(spec.getRawText()); recipe.setCachedRenderedText(null); diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java index 17622f6..6d606ee 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipesController.java @@ -9,6 +9,7 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeCommentService; import app.mealsmadeeasy.api.recipe.comment.RecipeCommentView; import app.mealsmadeeasy.api.recipe.converter.RecipeToFullViewConverter; import app.mealsmadeeasy.api.recipe.converter.RecipeToInfoViewConverter; +import app.mealsmadeeasy.api.recipe.converter.RecipeUpdateBodyToSpecConverter; import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStarService; @@ -43,6 +44,7 @@ public class RecipesController { private final ObjectMapper objectMapper; private final RecipeToFullViewConverter recipeToFullViewConverter; private final RecipeToInfoViewConverter recipeToInfoViewConverter; + private final RecipeUpdateBodyToSpecConverter updateBodyToSpecConverter; private Map getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) { Map wrapper = new HashMap<>(); @@ -72,7 +74,7 @@ public class RecipesController { @RequestBody RecipeUpdateBody updateBody, @AuthenticationPrincipal User principal ) { - final RecipeUpdateSpec spec = RecipeUpdateSpec.from(updateBody); + final RecipeUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody); final Recipe updated = this.recipeService.update(username, slug, spec, principal); final FullRecipeView view = this.recipeToFullViewConverter.convert(updated, includeRawText, principal); return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal)); diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/body/RecipeUpdateBody.java b/src/main/java/app/mealsmadeeasy/api/recipe/body/RecipeUpdateBody.java index 158511a..6c07da2 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/body/RecipeUpdateBody.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/body/RecipeUpdateBody.java @@ -3,6 +3,8 @@ package app.mealsmadeeasy.api.recipe.body; import lombok.Data; import org.jetbrains.annotations.Nullable; +import java.util.List; + @Data public class RecipeUpdateBody { @@ -12,10 +14,18 @@ public class RecipeUpdateBody { private String filename; } + @Data + public static class IngredientUpdateBody { + private @Nullable String amount; + private String name; + private @Nullable String notes; + } + private @Nullable String title; private @Nullable Integer preparationTime; private @Nullable Integer cookingTime; private @Nullable Integer totalTime; + private @Nullable List ingredients; private @Nullable String rawText; private @Nullable Boolean isPublic; private @Nullable MainImageUpdateBody mainImage; diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeToFullViewConverter.java b/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeToFullViewConverter.java index ffb0c3c..11ae8f9 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeToFullViewConverter.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeToFullViewConverter.java @@ -32,12 +32,26 @@ public class RecipeToFullViewConverter { .starCount(this.recipeService.getStarCount(recipe)) .viewerCount(this.recipeService.getViewerCount(recipe)) .isPublic(recipe.getIsPublic()); + + if (recipe.getIngredients() != null) { + b.ingredients(recipe.getIngredients().stream() + .map(ingredient -> FullRecipeView.IngredientView.builder() + .amount(ingredient.getAmount()) + .name(ingredient.getName()) + .notes(ingredient.getNotes()) + .build()) + .toList() + ); + } + if (recipe.getMainImage() != null) { b.mainImage(this.imageToViewConverter.convert(recipe.getMainImage(), viewer, false)); } + if (includeRawText) { b.rawText(recipe.getRawText()); } + return b.build(); } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeUpdateBodyToSpecConverter.java b/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeUpdateBodyToSpecConverter.java new file mode 100644 index 0000000..4a5742e --- /dev/null +++ b/src/main/java/app/mealsmadeeasy/api/recipe/converter/RecipeUpdateBodyToSpecConverter.java @@ -0,0 +1,44 @@ +package app.mealsmadeeasy.api.recipe.converter; + +import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody; +import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; +import org.jetbrains.annotations.Nullable; +import org.springframework.stereotype.Service; + +@Service +public class RecipeUpdateBodyToSpecConverter { + + public RecipeUpdateSpec convert(RecipeUpdateBody body) { + final var b = RecipeUpdateSpec.builder() + .title(body.getTitle()) + .preparationTime(body.getPreparationTime()) + .cookingTime(body.getCookingTime()) + .totalTime(body.getTotalTime()) + .rawText(body.getRawText()) + .isPublic(body.getIsPublic()); + + if (body.getIngredients() != null) { + b.ingredients(body.getIngredients().stream() + .map(ingredient -> RecipeUpdateSpec.IngredientUpdateSpec.builder() + .amount(ingredient.getAmount()) + .name(ingredient.getName()) + .notes(ingredient.getNotes()) + .build()) + .toList() + ); + } + + final @Nullable RecipeUpdateBody.MainImageUpdateBody mainImage = body.getMainImage(); + if (mainImage != null) { + b.mainImage( + RecipeUpdateSpec.MainImageUpdateSpec.builder() + .username(mainImage.getUsername()) + .filename(mainImage.getFilename()) + .build() + ); + } + + return b.build(); + } + +} diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeCreateSpec.java b/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeCreateSpec.java index a46c1c4..bb69f50 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeCreateSpec.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeCreateSpec.java @@ -5,15 +5,27 @@ import lombok.Builder; import lombok.Value; import org.jetbrains.annotations.Nullable; +import java.util.List; + @Value @Builder public class RecipeCreateSpec { + + @Builder + public record IngredientCreateSpec( + @Nullable String amount, + String name, + @Nullable String notes + ) {} + String slug; String title; @Nullable Integer preparationTime; @Nullable Integer cookingTime; @Nullable Integer totalTime; + @Nullable List ingredients; String rawText; boolean isPublic; @Nullable Image mainImage; + } diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeUpdateSpec.java b/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeUpdateSpec.java index 9e8ec5a..41db88a 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeUpdateSpec.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/spec/RecipeUpdateSpec.java @@ -2,12 +2,13 @@ package app.mealsmadeeasy.api.recipe.spec; import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.recipe.Recipe; -import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody; import lombok.Builder; import lombok.Value; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import java.util.List; + // For now, we cannot change slug after creation. // In the future, we may be able to have redirects from // old slugs to new slugs. @@ -22,25 +23,8 @@ public class RecipeUpdateSpec { String filename; } - public static RecipeUpdateSpec from(RecipeUpdateBody body) { - final var b = RecipeUpdateSpec.builder() - .title(body.getTitle()) - .preparationTime(body.getPreparationTime()) - .cookingTime(body.getCookingTime()) - .totalTime(body.getTotalTime()) - .rawText(body.getRawText()) - .isPublic(body.getIsPublic()); - final @Nullable RecipeUpdateBody.MainImageUpdateBody mainImage = body.getMainImage(); - if (mainImage != null) { - b.mainImage( - MainImageUpdateSpec.builder() - .username(mainImage.getUsername()) - .filename(mainImage.getFilename()) - .build() - ); - } - return b.build(); - } + @Builder + public record IngredientUpdateSpec(@Nullable String amount, String name, @Nullable String notes) {} // For testing convenience only. @ApiStatus.Internal @@ -67,6 +51,7 @@ public class RecipeUpdateSpec { @Nullable Integer preparationTime; @Nullable Integer cookingTime; @Nullable Integer totalTime; + @Nullable List ingredients; String rawText; Boolean isPublic; @Nullable MainImageUpdateSpec mainImage; diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/view/FullRecipeView.java b/src/main/java/app/mealsmadeeasy/api/recipe/view/FullRecipeView.java index 57e8105..67d1450 100644 --- a/src/main/java/app/mealsmadeeasy/api/recipe/view/FullRecipeView.java +++ b/src/main/java/app/mealsmadeeasy/api/recipe/view/FullRecipeView.java @@ -1,7 +1,6 @@ package app.mealsmadeeasy.api.recipe.view; import app.mealsmadeeasy.api.image.view.ImageView; -import app.mealsmadeeasy.api.recipe.Recipe; import app.mealsmadeeasy.api.user.view.UserInfoView; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; @@ -9,39 +8,14 @@ import lombok.Value; import org.jetbrains.annotations.Nullable; import java.time.OffsetDateTime; +import java.util.List; @Value @Builder public class FullRecipeView { - public static FullRecipeView from( - Recipe recipe, - String renderedText, - boolean includeRawText, - int starCount, - int viewerCount, - @Nullable ImageView mainImage - ) { - final var b = FullRecipeView.builder() - .id(recipe.getId()) - .created(recipe.getCreated()) - .modified(recipe.getModified()) - .slug(recipe.getSlug()) - .title(recipe.getTitle()) - .preparationTime(recipe.getPreparationTime()) - .cookingTime(recipe.getCookingTime()) - .totalTime(recipe.getTotalTime()) - .text(renderedText) - .owner(UserInfoView.from(recipe.getOwner())) - .starCount(starCount) - .viewerCount(viewerCount) - .mainImage(mainImage) - .isPublic(recipe.getIsPublic()); - if (includeRawText) { - b.rawText(recipe.getRawText()); - } - return b.build(); - } + @Builder + public record IngredientView(@Nullable String amount, String name, @Nullable String notes) {} Integer id; OffsetDateTime created; @@ -51,6 +25,7 @@ public class FullRecipeView { @Nullable Integer preparationTime; @Nullable Integer cookingTime; @Nullable Integer totalTime; + @Nullable List ingredients; String text; @Nullable String rawText; UserInfoView owner; diff --git a/src/main/resources/db/migration/V13__add_ingredients_to_recipe.sql b/src/main/resources/db/migration/V13__add_ingredients_to_recipe.sql new file mode 100644 index 0000000..6016fd8 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_ingredients_to_recipe.sql @@ -0,0 +1 @@ +ALTER TABLE recipe ADD COLUMN ingredients JSONB; \ No newline at end of file