MME-9 Add ingredients to Recipe entity.

This commit is contained in:
Jesse Brault 2026-02-13 09:07:09 -06:00
parent a92084a35a
commit 98c89f6247
12 changed files with 291 additions and 61 deletions

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.MinIOTestsExtension;
import app.mealsmadeeasy.api.PostgresTestsExtension; import app.mealsmadeeasy.api.PostgresTestsExtension;
import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.image.ImageException; 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 // TODO: test prep/cooking/total times included
@SpringBootTest @SpringBootTest
@ExtendWith(PostgresTestsExtension.class) @ExtendWith(PostgresTestsExtension.class)
@ExtendWith(MinIOTestsExtension.class)
public class RecipeServiceTests { public class RecipeServiceTests {
@Autowired @Autowired
@ -107,16 +109,31 @@ public class RecipeServiceTests {
public void whenCreate_allFieldsTransferredFromSpec() throws Exception { public void whenCreate_allFieldsTransferredFromSpec() throws Exception {
final User owner = this.seedUser(); final User owner = this.seedUser();
final Image image = this.seedImage(owner); final Image image = this.seedImage(owner);
final List<RecipeCreateSpec.IngredientCreateSpec> 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() final RecipeCreateSpec spec = RecipeCreateSpec.builder()
.title("Recipe Title") .title("Recipe Title")
.slug("recipe-slug") .slug("recipe-slug")
.preparationTime(15) .preparationTime(15)
.cookingTime(30) .cookingTime(30)
.totalTime(45) .totalTime(45)
.ingredients(ingredients)
.rawText("# Hello Recipe") .rawText("# Hello Recipe")
.isPublic(true) .isPublic(true)
.mainImage(image) .mainImage(image)
.build(); .build();
final Recipe recipe = this.recipeService.create(owner, spec, false); final Recipe recipe = this.recipeService.create(owner, spec, false);
assertThat(recipe.getId(), is(notNullValue())); assertThat(recipe.getId(), is(notNullValue()));
assertThat(recipe.getTitle(), is("Recipe Title")); assertThat(recipe.getTitle(), is("Recipe Title"));
@ -124,6 +141,8 @@ public class RecipeServiceTests {
assertThat(recipe.getPreparationTime(), is(15)); assertThat(recipe.getPreparationTime(), is(15));
assertThat(recipe.getCookingTime(), is(30)); assertThat(recipe.getCookingTime(), is(30));
assertThat(recipe.getTotalTime(), is(45)); assertThat(recipe.getTotalTime(), is(45));
assertThat(recipe.getIngredients(), is(notNullValue()));
assertThat(recipe.getIngredients().size(), is(2));
assertThat(recipe.getRawText(), is("# Hello Recipe")); assertThat(recipe.getRawText(), is("# Hello Recipe"));
assertThat(recipe.getMainImage(), is(notNullValue())); assertThat(recipe.getMainImage(), is(notNullValue()));
assertThat(recipe.getMainImage().getId(), is(image.getId())); assertThat(recipe.getMainImage().getId(), is(image.getId()));
@ -305,6 +324,61 @@ public class RecipeServiceTests {
assertThat(viewableInfos, containsRecipes(r0, r1)); 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<RecipeUpdateSpec.IngredientUpdateSpec> 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 @Test
public void updateRawText() { public void updateRawText() {
final User owner = this.seedUser(); final User owner = this.seedUser();

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.MinIOTestsExtension;
import app.mealsmadeeasy.api.PostgresTestsExtension; import app.mealsmadeeasy.api.PostgresTestsExtension;
import app.mealsmadeeasy.api.auth.AuthService; import app.mealsmadeeasy.api.auth.AuthService;
import app.mealsmadeeasy.api.auth.LoginDetails; import app.mealsmadeeasy.api.auth.LoginDetails;
@ -25,6 +26,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
@ -35,6 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@ExtendWith(PostgresTestsExtension.class) @ExtendWith(PostgresTestsExtension.class)
@ExtendWith(MinIOTestsExtension.class)
public class RecipesControllerTests { public class RecipesControllerTests {
private static InputStream getHal9000() { private static InputStream getHal9000() {
@ -222,13 +225,39 @@ public class RecipesControllerTests {
public void updateRecipe() throws Exception { public void updateRecipe() throws Exception {
final User owner = this.seedUser(); final User owner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner, false); final Recipe recipe = this.createTestRecipe(owner, false);
final String accessToken = this.getAccessToken(owner); final Image image = this.createHal9000(owner);
final String body = this.getUpdateBody();
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( this.mockMvc.perform(
post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug()) post("/recipes/{username}/{slug}", owner.getUsername(), recipe.getSlug())
.header("Authorization", "Bearer " + accessToken) .header("Authorization", "Bearer " + this.getAccessToken(owner))
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(body) .content(updateBody)
) )
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.recipe.id").value(recipe.getId())) .andExpect(jsonPath("$.recipe.id").value(recipe.getId()))
@ -236,6 +265,15 @@ public class RecipesControllerTests {
.andExpect(jsonPath("$.recipe.preparationTime").value(15)) .andExpect(jsonPath("$.recipe.preparationTime").value(15))
.andExpect(jsonPath("$.recipe.cookingTime").value(30)) .andExpect(jsonPath("$.recipe.cookingTime").value(30))
.andExpect(jsonPath("$.recipe.totalTime").value(45)) .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("<h1>Hello, Updated World!</h1>")) .andExpect(jsonPath("$.recipe.text").value("<h1>Hello, Updated World!</h1>"))
.andExpect(jsonPath("$.recipe.rawText").value("# Hello, Updated World!")) .andExpect(jsonPath("$.recipe.rawText").value("# Hello, Updated World!"))
.andExpect(jsonPath("$.recipe.owner.id").value(owner.getId())) .andExpect(jsonPath("$.recipe.owner.id").value(owner.getId()))
@ -243,7 +281,10 @@ public class RecipesControllerTests {
.andExpect(jsonPath("$.recipe.starCount").value(0)) .andExpect(jsonPath("$.recipe.starCount").value(0))
.andExpect(jsonPath("$.recipe.viewerCount").value(0)) .andExpect(jsonPath("$.recipe.viewerCount").value(0))
.andExpect(jsonPath("$.recipe.public").value(true)) .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("$.isStarred").value(false))
.andExpect(jsonPath("$.isOwner").value(true)); .andExpect(jsonPath("$.isOwner").value(true));
} }

View File

@ -4,14 +4,17 @@ import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.annotations.Type;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -21,20 +24,54 @@ import java.util.Set;
@ToString @ToString
public class Recipe { 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 @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false) @Basic(optional = false)
@Column(updatable = false)
private Integer id; private Integer id;
@Column(nullable = false) @Basic(optional = false)
private OffsetDateTime created; private OffsetDateTime created;
private OffsetDateTime modified; private OffsetDateTime modified;
@Column(nullable = false, unique = true) @Basic(optional = false)
@Column(unique = true)
private String slug; private String slug;
@Column(nullable = false) @Basic(optional = false)
private String title; private String title;
@Nullable @Nullable
@ -46,7 +83,13 @@ public class Recipe {
@Nullable @Nullable
private Integer totalTime; private Integer totalTime;
@Column(columnDefinition = "TEXT", nullable = false) @Type(JsonBinaryType.class)
@Column(columnDefinition = "JSONB")
@Nullable
private List<Ingredient> ingredients;
@Basic(optional = false)
@Column(columnDefinition = "TEXT")
private String rawText; private String rawText;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
@ -63,7 +106,7 @@ public class Recipe {
@OneToMany(mappedBy = "recipe", orphanRemoval = true, cascade = CascadeType.ALL) @OneToMany(mappedBy = "recipe", orphanRemoval = true, cascade = CascadeType.ALL)
private Set<RecipeComment> comments = new HashSet<>(); private Set<RecipeComment> comments = new HashSet<>();
@Column(nullable = false) @Basic(optional = false)
private Boolean isPublic = false; private Boolean isPublic = false;
@ManyToMany @ManyToMany

View File

@ -50,6 +50,20 @@ public class RecipeService {
draft.setOwner(owner); draft.setOwner(owner);
draft.setSlug(spec.getSlug()); draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle()); 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.setRawText(spec.getRawText());
draft.setMainImage(spec.getMainImage()); draft.setMainImage(spec.getMainImage());
draft.setIsPublic(spec.isPublic()); draft.setIsPublic(spec.isPublic());
@ -57,12 +71,14 @@ public class RecipeService {
draft.setCookingTime(spec.getCookingTime()); draft.setCookingTime(spec.getCookingTime());
draft.setTotalTime(spec.getTotalTime()); draft.setTotalTime(spec.getTotalTime());
final Recipe saved = this.recipeRepository.save(draft); final Recipe saved = this.recipeRepository.save(draft);
if (queueSummaryJob) { if (queueSummaryJob) {
this.jobService.create( this.jobService.create(
RecipeSummaryJobHandler.JOB_KEY, RecipeSummaryJobHandler.JOB_KEY,
new RecipeSummaryJobHandler.RecipeSummaryJobPayload(saved.getId()) new RecipeSummaryJobHandler.RecipeSummaryJobPayload(saved.getId())
); );
} }
return saved; return saved;
} }
@ -160,6 +176,19 @@ public class RecipeService {
recipe.setTotalTime(spec.getTotalTime()); 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) { if (spec.getRawText() != null) {
recipe.setRawText(spec.getRawText()); recipe.setRawText(spec.getRawText());
recipe.setCachedRenderedText(null); recipe.setCachedRenderedText(null);

View File

@ -9,6 +9,7 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeCommentService;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentView; import app.mealsmadeeasy.api.recipe.comment.RecipeCommentView;
import app.mealsmadeeasy.api.recipe.converter.RecipeToFullViewConverter; import app.mealsmadeeasy.api.recipe.converter.RecipeToFullViewConverter;
import app.mealsmadeeasy.api.recipe.converter.RecipeToInfoViewConverter; 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.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarService; import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
@ -43,6 +44,7 @@ public class RecipesController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RecipeToFullViewConverter recipeToFullViewConverter; private final RecipeToFullViewConverter recipeToFullViewConverter;
private final RecipeToInfoViewConverter recipeToInfoViewConverter; private final RecipeToInfoViewConverter recipeToInfoViewConverter;
private final RecipeUpdateBodyToSpecConverter updateBodyToSpecConverter;
private Map<String, Object> getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) { private Map<String, Object> getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) {
Map<String, Object> wrapper = new HashMap<>(); Map<String, Object> wrapper = new HashMap<>();
@ -72,7 +74,7 @@ public class RecipesController {
@RequestBody RecipeUpdateBody updateBody, @RequestBody RecipeUpdateBody updateBody,
@AuthenticationPrincipal User principal @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 Recipe updated = this.recipeService.update(username, slug, spec, principal);
final FullRecipeView view = this.recipeToFullViewConverter.convert(updated, includeRawText, principal); final FullRecipeView view = this.recipeToFullViewConverter.convert(updated, includeRawText, principal);
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal)); return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal));

View File

@ -3,6 +3,8 @@ package app.mealsmadeeasy.api.recipe.body;
import lombok.Data; import lombok.Data;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List;
@Data @Data
public class RecipeUpdateBody { public class RecipeUpdateBody {
@ -12,10 +14,18 @@ public class RecipeUpdateBody {
private String filename; 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 String title;
private @Nullable Integer preparationTime; private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime; private @Nullable Integer cookingTime;
private @Nullable Integer totalTime; private @Nullable Integer totalTime;
private @Nullable List<IngredientUpdateBody> ingredients;
private @Nullable String rawText; private @Nullable String rawText;
private @Nullable Boolean isPublic; private @Nullable Boolean isPublic;
private @Nullable MainImageUpdateBody mainImage; private @Nullable MainImageUpdateBody mainImage;

View File

@ -32,12 +32,26 @@ public class RecipeToFullViewConverter {
.starCount(this.recipeService.getStarCount(recipe)) .starCount(this.recipeService.getStarCount(recipe))
.viewerCount(this.recipeService.getViewerCount(recipe)) .viewerCount(this.recipeService.getViewerCount(recipe))
.isPublic(recipe.getIsPublic()); .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) { if (recipe.getMainImage() != null) {
b.mainImage(this.imageToViewConverter.convert(recipe.getMainImage(), viewer, false)); b.mainImage(this.imageToViewConverter.convert(recipe.getMainImage(), viewer, false));
} }
if (includeRawText) { if (includeRawText) {
b.rawText(recipe.getRawText()); b.rawText(recipe.getRawText());
} }
return b.build(); return b.build();
} }

View File

@ -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();
}
}

View File

@ -5,15 +5,27 @@ import lombok.Builder;
import lombok.Value; import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List;
@Value @Value
@Builder @Builder
public class RecipeCreateSpec { public class RecipeCreateSpec {
@Builder
public record IngredientCreateSpec(
@Nullable String amount,
String name,
@Nullable String notes
) {}
String slug; String slug;
String title; String title;
@Nullable Integer preparationTime; @Nullable Integer preparationTime;
@Nullable Integer cookingTime; @Nullable Integer cookingTime;
@Nullable Integer totalTime; @Nullable Integer totalTime;
@Nullable List<IngredientCreateSpec> ingredients;
String rawText; String rawText;
boolean isPublic; boolean isPublic;
@Nullable Image mainImage; @Nullable Image mainImage;
} }

View File

@ -2,12 +2,13 @@ package app.mealsmadeeasy.api.recipe.spec;
import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.recipe.Recipe; import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody;
import lombok.Builder; import lombok.Builder;
import lombok.Value; import lombok.Value;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.List;
// For now, we cannot change slug after creation. // For now, we cannot change slug after creation.
// In the future, we may be able to have redirects from // In the future, we may be able to have redirects from
// old slugs to new slugs. // old slugs to new slugs.
@ -22,25 +23,8 @@ public class RecipeUpdateSpec {
String filename; String filename;
} }
public static RecipeUpdateSpec from(RecipeUpdateBody body) { @Builder
final var b = RecipeUpdateSpec.builder() public record IngredientUpdateSpec(@Nullable String amount, String name, @Nullable String notes) {}
.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();
}
// For testing convenience only. // For testing convenience only.
@ApiStatus.Internal @ApiStatus.Internal
@ -67,6 +51,7 @@ public class RecipeUpdateSpec {
@Nullable Integer preparationTime; @Nullable Integer preparationTime;
@Nullable Integer cookingTime; @Nullable Integer cookingTime;
@Nullable Integer totalTime; @Nullable Integer totalTime;
@Nullable List<IngredientUpdateSpec> ingredients;
String rawText; String rawText;
Boolean isPublic; Boolean isPublic;
@Nullable MainImageUpdateSpec mainImage; @Nullable MainImageUpdateSpec mainImage;

View File

@ -1,7 +1,6 @@
package app.mealsmadeeasy.api.recipe.view; package app.mealsmadeeasy.api.recipe.view;
import app.mealsmadeeasy.api.image.view.ImageView; import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder; import lombok.Builder;
@ -9,39 +8,14 @@ import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List;
@Value @Value
@Builder @Builder
public class FullRecipeView { public class FullRecipeView {
public static FullRecipeView from( @Builder
Recipe recipe, public record IngredientView(@Nullable String amount, String name, @Nullable String notes) {}
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();
}
Integer id; Integer id;
OffsetDateTime created; OffsetDateTime created;
@ -51,6 +25,7 @@ public class FullRecipeView {
@Nullable Integer preparationTime; @Nullable Integer preparationTime;
@Nullable Integer cookingTime; @Nullable Integer cookingTime;
@Nullable Integer totalTime; @Nullable Integer totalTime;
@Nullable List<IngredientView> ingredients;
String text; String text;
@Nullable String rawText; @Nullable String rawText;
UserInfoView owner; UserInfoView owner;

View File

@ -0,0 +1 @@
ALTER TABLE recipe ADD COLUMN ingredients JSONB;