Clean up recipe classes.

This commit is contained in:
Jesse Brault 2026-01-15 22:18:08 -06:00
parent 70c560f0cb
commit 51cae79daa
31 changed files with 372 additions and 650 deletions

View File

@ -93,14 +93,15 @@ public class RecipeControllerTests {
} }
private Recipe createTestRecipe(User owner, boolean isPublic) { private Recipe createTestRecipe(User owner, boolean isPublic) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = RecipeCreateSpec.builder()
spec.setSlug(UUID.randomUUID().toString()); .slug(UUID.randomUUID().toString())
spec.setTitle("Test Recipe"); .title("Test Recipe")
spec.setPreparationTime(10); .preparationTime(10)
spec.setCookingTime(20); .cookingTime(20)
spec.setTotalTime(30); .totalTime(30)
spec.setRawText("# Hello, World!"); .rawText("# Hello, World!")
spec.setPublic(isPublic); .isPublic(isPublic)
.build();
return this.recipeService.create(owner, spec); return this.recipeService.create(owner, spec);
} }
@ -146,7 +147,7 @@ public class RecipeControllerTests {
.andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername())) .andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername()))
.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.isPublic").value(true)) .andExpect(jsonPath("$.recipe.public").value(true))
.andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) .andExpect(jsonPath("$.recipe.mainImage").value(nullValue()))
.andExpect(jsonPath("$.isStarred").value(nullValue())) .andExpect(jsonPath("$.isStarred").value(nullValue()))
.andExpect(jsonPath("$.isOwner").value(nullValue())); .andExpect(jsonPath("$.isOwner").value(nullValue()));
@ -225,13 +226,14 @@ public class RecipeControllerTests {
} }
private String getUpdateBody() throws JsonProcessingException { private String getUpdateBody() throws JsonProcessingException {
final RecipeUpdateSpec spec = new RecipeUpdateSpec(); final RecipeUpdateSpec spec = RecipeUpdateSpec.builder()
spec.setTitle("Updated Test Recipe"); .title("Updated Test Recipe")
spec.setPreparationTime(15); .preparationTime(15)
spec.setCookingTime(30); .cookingTime(30)
spec.setTotalTime(45); .totalTime(45)
spec.setRawText("# Hello, Updated World!"); .rawText("# Hello, Updated World!")
spec.setIsPublic(true); .isPublic(true)
.build();
return this.objectMapper.writeValueAsString(spec); return this.objectMapper.writeValueAsString(spec);
} }
@ -259,7 +261,7 @@ public class RecipeControllerTests {
.andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername())) .andExpect(jsonPath("$.recipe.owner.username").value(owner.getUsername()))
.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.isPublic").value(true)) .andExpect(jsonPath("$.recipe.public").value(true))
.andExpect(jsonPath("$.recipe.mainImage").value(nullValue())) .andExpect(jsonPath("$.recipe.mainImage").value(nullValue()))
.andExpect(jsonPath("$.isStarred").value(false)) .andExpect(jsonPath("$.isStarred").value(false))
.andExpect(jsonPath("$.isOwner").value(true)); .andExpect(jsonPath("$.isOwner").value(true));
@ -271,21 +273,25 @@ public class RecipeControllerTests {
final Image hal9000 = this.createHal9000(owner); final Image hal9000 = this.createHal9000(owner);
final RecipeCreateSpec createSpec = new RecipeCreateSpec(); final RecipeCreateSpec createSpec = RecipeCreateSpec.builder()
createSpec.setTitle("Test Recipe"); .title("Test Recipe")
createSpec.setSlug("test-recipe"); .slug(UUID.randomUUID().toString())
createSpec.setPublic(false); .isPublic(false)
createSpec.setRawText("# Hello, World!"); .rawText("# Hello, World!")
createSpec.setMainImage(hal9000); .mainImage(hal9000)
.build();
Recipe recipe = this.recipeService.create(owner, createSpec); Recipe recipe = this.recipeService.create(owner, createSpec);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); final RecipeUpdateSpec updateSpec = RecipeUpdateSpec.builder()
updateSpec.setTitle("Updated Test Recipe"); .title("Updated Test Recipe")
updateSpec.setRawText("# Hello, Updated World!"); .rawText("# Hello, Updated World!")
final RecipeUpdateSpec.MainImageUpdateSpec mainImageUpdateSpec = new RecipeUpdateSpec.MainImageUpdateSpec(); .mainImage(
mainImageUpdateSpec.setUsername(hal9000.getOwner().getUsername()); RecipeUpdateSpec.MainImageUpdateSpec.builder()
mainImageUpdateSpec.setFilename(hal9000.getUserFilename()); .username(hal9000.getOwner().getUsername())
updateSpec.setMainImage(mainImageUpdateSpec); .filename(hal9000.getUserFilename())
.build()
)
.build();
final String body = this.objectMapper.writeValueAsString(updateSpec); final String body = this.objectMapper.writeValueAsString(updateSpec);
final String accessToken = this.getAccessToken(owner); final String accessToken = this.getAccessToken(owner);

View File

@ -58,11 +58,12 @@ public class RecipeServiceTests {
} }
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) { private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = RecipeCreateSpec.builder()
spec.setSlug(UUID.randomUUID().toString()); .slug(UUID.randomUUID().toString())
spec.setTitle("My Recipe"); .title("My Recipe")
spec.setRawText("Hello!"); .rawText("Hello!")
spec.setPublic(isPublic); .isPublic(isPublic)
.build();
return this.recipeService.create(owner, spec); return this.recipeService.create(owner, spec);
} }
@ -80,7 +81,9 @@ public class RecipeServiceTests {
@Test @Test
public void createWithoutOwnerThrowsAccessDenied() { public void createWithoutOwnerThrowsAccessDenied() {
assertThrows(AccessDeniedException.class, () -> this.recipeService.create(null, new RecipeCreateSpec())); assertThrows(AccessDeniedException.class, () -> this.recipeService.create(
null, RecipeCreateSpec.builder().build()
));
} }
@Test @Test
@ -156,8 +159,9 @@ public class RecipeServiceTests {
final User owner = this.seedUser(); final User owner = this.seedUser();
final User viewer = this.seedUser(); final User viewer = this.seedUser();
final Recipe notYetPublicRecipe = this.createTestRecipe(owner); final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(notYetPublicRecipe); final RecipeUpdateSpec updateSpec = RecipeUpdateSpec.fromRecipeToBuilder(notYetPublicRecipe)
updateSpec.setIsPublic(true); .isPublic(true)
.build();
final Recipe publicRecipe = this.recipeService.update( final Recipe publicRecipe = this.recipeService.update(
notYetPublicRecipe.getOwner().getUsername(), notYetPublicRecipe.getOwner().getUsername(),
notYetPublicRecipe.getSlug(), notYetPublicRecipe.getSlug(),
@ -284,14 +288,16 @@ public class RecipeServiceTests {
@Test @Test
public void updateRawText() throws RecipeException, ImageException { public void updateRawText() throws RecipeException, ImageException {
final User owner = this.seedUser(); final User owner = this.seedUser();
final RecipeCreateSpec createSpec = new RecipeCreateSpec(); final RecipeCreateSpec createSpec = RecipeCreateSpec.builder()
createSpec.setSlug("my-recipe"); .slug(UUID.randomUUID().toString())
createSpec.setTitle("My Recipe"); .title("My Recipe")
createSpec.setRawText("# A Heading"); .rawText("# A Heading")
.build();
Recipe recipe = this.recipeService.create(owner, createSpec); Recipe recipe = this.recipeService.create(owner, createSpec);
final String newRawText = "# A Heading\n## A Subheading"; final String newRawText = "# A Heading\n## A Subheading";
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(recipe); final RecipeUpdateSpec updateSpec = RecipeUpdateSpec.fromRecipeToBuilder(recipe)
updateSpec.setRawText(newRawText); .rawText(newRawText)
.build();
recipe = this.recipeService.update( recipe = this.recipeService.update(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
@ -306,8 +312,9 @@ public class RecipeServiceTests {
final User owner = this.seedUser(); final User owner = this.seedUser();
final User notOwner = this.seedUser(); final User notOwner = this.seedUser();
final Recipe recipe = this.createTestRecipe(owner); final Recipe recipe = this.createTestRecipe(owner);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec(); final RecipeUpdateSpec updateSpec = RecipeUpdateSpec.fromRecipeToBuilder(recipe)
updateSpec.setRawText("should fail"); .rawText("should fail")
.build();
assertThrows( assertThrows(
AccessDeniedException.class, AccessDeniedException.class,
() -> this.recipeService.update( () -> this.recipeService.update(

View File

@ -56,7 +56,7 @@ public class RecipeStarRepositoryTests {
final RecipeStar starDraft = new RecipeStar(); final RecipeStar starDraft = new RecipeStar();
final RecipeStarId starId = new RecipeStarId(); final RecipeStarId starId = new RecipeStarId();
starId.setRecipeId(recipe.getId()); starId.setRecipeId(recipe.getId());
starId.getOwnerId(owner.getId()); starId.setOwnerId(owner.getId());
starDraft.setId(starId); starDraft.setId(starId);
this.recipeStarRepository.save(starDraft); this.recipeStarRepository.save(starDraft);

View File

@ -44,11 +44,12 @@ public class RecipeStarServiceTests {
} }
private Recipe seedRecipe(User owner) { private Recipe seedRecipe(User owner) {
final RecipeCreateSpec spec = new RecipeCreateSpec(); final RecipeCreateSpec spec = RecipeCreateSpec.builder()
spec.setSlug(UUID.randomUUID().toString()); .slug(UUID.randomUUID().toString())
spec.setTitle("Test Recipe"); .title("Test Recipe")
spec.setRawText("My great recipe has five ingredients."); .rawText("My great recipe has five ingredients.")
spec.setPublic(true); .isPublic(true)
.build();
return this.recipeService.create(owner, spec); return this.recipeService.create(owner, spec);
} }

View File

@ -102,12 +102,13 @@ public class DevConfiguration {
logger.info("Created mainImage {} for {}", mainImage, recipePath); logger.info("Created mainImage {} for {}", mainImage, recipePath);
} }
final RecipeCreateSpec recipeCreateSpec = new RecipeCreateSpec(); final RecipeCreateSpec recipeCreateSpec = RecipeCreateSpec.builder()
recipeCreateSpec.setSlug(frontMatter.slug); .slug(frontMatter.slug)
recipeCreateSpec.setTitle(frontMatter.title); .title(frontMatter.title)
recipeCreateSpec.setRawText(rawRecipeText); .rawText(rawRecipeText)
recipeCreateSpec.setPublic(frontMatter.isPublic); .isPublic(frontMatter.isPublic)
recipeCreateSpec.setMainImage(mainImage); .mainImage(mainImage)
.build();
final Recipe recipe = this.recipeService.create(testUser, recipeCreateSpec); final Recipe recipe = this.recipeService.create(testUser, recipeCreateSpec);
logger.info("Created recipe {}", recipe); logger.info("Created recipe {}", recipe);
} }

View File

@ -75,6 +75,7 @@ public final class Recipe {
@ManyToOne @ManyToOne
@JoinColumn(name = "main_image_id") @JoinColumn(name = "main_image_id")
@Nullable
private Image mainImage; private Image mainImage;
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)

View File

@ -1,12 +1,13 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.ImageException; import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
import app.mealsmadeeasy.api.recipe.body.RecipeSearchBody; import app.mealsmadeeasy.api.recipe.body.RecipeSearchBody;
import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment; import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentCreateBody; import app.mealsmadeeasy.api.recipe.comment.RecipeCommentCreateBody;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentService; 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.spec.RecipeAiSearchSpec;
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;
@ -94,10 +95,11 @@ public class RecipeController {
@PathVariable String username, @PathVariable String username,
@PathVariable String slug, @PathVariable String slug,
@RequestParam(defaultValue = "true") boolean includeRawText, @RequestParam(defaultValue = "true") boolean includeRawText,
@RequestBody RecipeUpdateSpec updateSpec, @RequestBody RecipeUpdateBody updateBody,
@AuthenticationPrincipal User principal @AuthenticationPrincipal User principal
) throws ImageException, RecipeException { ) throws ImageException, RecipeException {
final Recipe updated = this.recipeService.update(username, slug, updateSpec, principal); final RecipeUpdateSpec spec = RecipeUpdateSpec.from(updateBody);
final Recipe updated = this.recipeService.update(username, slug, spec, principal);
final FullRecipeView view = this.recipeService.toFullRecipeView(updated, includeRawText, principal); final FullRecipeView view = this.recipeService.toFullRecipeView(updated, includeRawText, principal);
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal)); return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal));
} }
@ -117,9 +119,9 @@ public class RecipeController {
@AuthenticationPrincipal User user @AuthenticationPrincipal User user
) { ) {
if (recipeSearchBody.getType() == RecipeSearchBody.Type.AI_PROMPT) { if (recipeSearchBody.getType() == RecipeSearchBody.Type.AI_PROMPT) {
final RecipeAiSearchSpec spec = this.objectMapper.convertValue( final RecipeAiSearchBody spec = this.objectMapper.convertValue(
recipeSearchBody.getData(), recipeSearchBody.getData(),
RecipeAiSearchSpec.class RecipeAiSearchBody.class
); );
final List<RecipeInfoView> results = this.recipeService.aiSearch(spec, user); final List<RecipeInfoView> results = this.recipeService.aiSearch(spec, user);
return ResponseEntity.ok(Map.of("results", results)); return ResponseEntity.ok(Map.of("results", results));

View File

@ -10,7 +10,7 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface RecipeRepository extends JpaRepository<Recipe, Long> { public interface RecipeRepository extends JpaRepository<Recipe, Integer> {
List<Recipe> findAllByIsPublicIsTrue(); List<Recipe> findAllByIsPublicIsTrue();

View File

@ -5,9 +5,9 @@ import org.jetbrains.annotations.Nullable;
public interface RecipeSecurity { public interface RecipeSecurity {
boolean isOwner(Recipe recipe, User user); boolean isOwner(Recipe recipe, User user);
boolean isOwner(long recipeId, User user) throws RecipeException; boolean isOwner(Integer recipeId, User user) throws RecipeException;
boolean isOwner(String username, String slug, @Nullable User user) throws RecipeException; boolean isOwner(String username, String slug, @Nullable User user) throws RecipeException;
boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException; boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException;
boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException; boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException;
boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException; boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException;
} }

View File

@ -21,7 +21,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
} }
@Override @Override
public boolean isOwner(long recipeId, User user) throws RecipeException { public boolean isOwner(Integer recipeId, User user) throws RecipeException {
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException( final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, RecipeException.Type.INVALID_ID,
"No such Recipe with id " + recipeId "No such Recipe with id " + recipeId
@ -78,7 +78,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
} }
@Override @Override
public boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException { public boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException {
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException( final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, RecipeException.Type.INVALID_ID,
"No such Recipe with id: " + recipeId "No such Recipe with id: " + recipeId

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.ImageException; import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.recipe.spec.RecipeAiSearchSpec; import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.view.FullRecipeView; import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
@ -19,11 +19,11 @@ public interface RecipeService {
Recipe create(@Nullable User owner, RecipeCreateSpec spec); Recipe create(@Nullable User owner, RecipeCreateSpec spec);
Recipe getById(long id, @Nullable User viewer) throws RecipeException; Recipe getById(Integer id, @Nullable User viewer) throws RecipeException;
Recipe getByIdWithStars(long id, @Nullable User viewer) throws RecipeException; Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException;
Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException; Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException;
FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException; FullRecipeView getFullViewById(Integer id, @Nullable User viewer) throws RecipeException;
FullRecipeView getFullViewByUsernameAndSlug( FullRecipeView getFullViewByUsernameAndSlug(
String username, String username,
String slug, String slug,
@ -37,16 +37,16 @@ public interface RecipeService {
List<Recipe> getRecipesViewableBy(User viewer); List<Recipe> getRecipesViewableBy(User viewer);
List<Recipe> getRecipesOwnedBy(User owner); List<Recipe> getRecipesOwnedBy(User owner);
List<RecipeInfoView> aiSearch(RecipeAiSearchSpec searchSpec, @Nullable User viewer); List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer);
Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier) Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
throws RecipeException, ImageException; throws RecipeException, ImageException;
Recipe addViewer(long id, User modifier, User viewer) throws RecipeException; Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException;
Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException; Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException;
Recipe clearAllViewers(long id, User modifier) throws RecipeException; Recipe clearAllViewers(Integer id, User modifier) throws RecipeException;
void deleteRecipe(long id, User modifier); void deleteRecipe(Integer id, User modifier);
FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer); FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer);
RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer); RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer);

View File

@ -5,7 +5,7 @@ import app.mealsmadeeasy.api.image.ImageException;
import app.mealsmadeeasy.api.image.ImageService; import app.mealsmadeeasy.api.image.ImageService;
import app.mealsmadeeasy.api.image.view.ImageView; import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.markdown.MarkdownService; import app.mealsmadeeasy.api.markdown.MarkdownService;
import app.mealsmadeeasy.api.recipe.spec.RecipeAiSearchSpec; import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec; import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository; import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository;
@ -58,16 +58,16 @@ public class RecipeServiceImpl implements RecipeService {
} }
final Recipe draft = new Recipe(); final Recipe draft = new Recipe();
draft.setCreated(OffsetDateTime.now()); draft.setCreated(OffsetDateTime.now());
draft.setOwner((User) owner); draft.setOwner(owner);
draft.setSlug(spec.getSlug()); draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle()); draft.setTitle(spec.getTitle());
draft.setRawText(spec.getRawText()); draft.setRawText(spec.getRawText());
draft.setMainImage((Image) spec.getMainImage()); draft.setMainImage(spec.getMainImage());
draft.setIsPublic(spec.isPublic()); draft.setIsPublic(spec.isPublic());
return this.recipeRepository.save(draft); return this.recipeRepository.save(draft);
} }
private Recipe findRecipeEntity(long id) throws RecipeException { private Recipe findRecipeEntity(Integer id) throws RecipeException {
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException( return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
)); ));
@ -75,13 +75,13 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getById(long id, User viewer) throws RecipeException { public Recipe getById(Integer id, User viewer) throws RecipeException {
return this.findRecipeEntity(id); return this.findRecipeEntity(id);
} }
@Override @Override
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getByIdWithStars(long id, @Nullable User viewer) throws RecipeException { public Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException {
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException( return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, RecipeException.Type.INVALID_ID,
"No such Recipe with id: " + id "No such Recipe with id: " + id
@ -145,7 +145,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@PreAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)") @PreAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)")
public FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException { public FullRecipeView getFullViewById(Integer id, @Nullable User viewer) throws RecipeException {
final Recipe recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException( final Recipe recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
)); ));
@ -170,7 +170,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) { public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
return this.recipeRepository.findAllViewableBy((User) viewer, pageable).map(recipe -> return this.recipeRepository.findAllViewableBy(viewer, pageable).map(recipe ->
this.getInfoView(recipe, viewer) this.getInfoView(recipe, viewer)
); );
} }
@ -178,7 +178,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
public List<Recipe> getByMinimumStars(long minimumStars, User viewer) { public List<Recipe> getByMinimumStars(long minimumStars, User viewer) {
return List.copyOf( return List.copyOf(
this.recipeRepository.findAllViewableByStarsGreaterThanEqual(minimumStars, (User) viewer) this.recipeRepository.findAllViewableByStarsGreaterThanEqual(minimumStars, viewer)
); );
} }
@ -189,16 +189,16 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
public List<Recipe> getRecipesViewableBy(User viewer) { public List<Recipe> getRecipesViewableBy(User viewer) {
return List.copyOf(this.recipeRepository.findAllByViewersContaining((User) viewer)); return List.copyOf(this.recipeRepository.findAllByViewersContaining(viewer));
} }
@Override @Override
public List<Recipe> getRecipesOwnedBy(User owner) { public List<Recipe> getRecipesOwnedBy(User owner) {
return List.copyOf(this.recipeRepository.findAllByOwner((User) owner)); return List.copyOf(this.recipeRepository.findAllByOwner(owner));
} }
@Override @Override
public List<RecipeInfoView> aiSearch(RecipeAiSearchSpec searchSpec, @Nullable User viewer) { public List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt()); final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
final List<Recipe> results; final List<Recipe> results;
if (viewer == null) { if (viewer == null) {
@ -211,6 +211,51 @@ public class RecipeServiceImpl implements RecipeService {
.toList(); .toList();
} }
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) throws ImageException {
boolean didUpdate = false;
if (spec.getTitle() != null) {
recipe.setTitle(spec.getTitle());
didUpdate = true;
}
if (spec.getPreparationTime() != null) {
recipe.setPreparationTime(spec.getPreparationTime());
didUpdate = true;
}
if (spec.getCookingTime() != null) {
recipe.setCookingTime(spec.getCookingTime());
didUpdate = true;
}
if (spec.getTotalTime() != null) {
recipe.setTotalTime(spec.getTotalTime());
didUpdate = true;
}
if (spec.getRawText() != null) {
recipe.setRawText(spec.getRawText());
recipe.setCachedRenderedText(null);
didUpdate = true;
}
if (spec.getIsPublic() != null) {
recipe.setIsPublic(spec.getIsPublic());
didUpdate = true;
}
// TODO: we have to think about how to unset the main image vs. just leaving it out of the request
if (spec.getMainImage() != null) {
final Image mainImage = this.imageService.getByUsernameAndFilename(
spec.getMainImage().getUsername(),
spec.getMainImage().getFilename(),
modifier
);
recipe.setMainImage(mainImage);
}
if (didUpdate) {
recipe.setModified(OffsetDateTime.now());
}
}
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier) public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
@ -221,56 +266,35 @@ public class RecipeServiceImpl implements RecipeService {
"No such Recipe for username " + username + " and slug: " + slug "No such Recipe for username " + username + " and slug: " + slug
) )
); );
this.prepareForUpdate(spec, recipe, modifier);
recipe.setTitle(spec.getTitle());
recipe.setPreparationTime(spec.getPreparationTime());
recipe.setCookingTime(spec.getCookingTime());
recipe.setTotalTime(spec.getTotalTime());
recipe.setRawText(spec.getRawText());
recipe.setCachedRenderedText(null);
recipe.setIsPublic(spec.getIsPublic());
final Image mainImage;
if (spec.getMainImage() == null) {
mainImage = null;
} else {
mainImage = (Image) this.imageService.getByUsernameAndFilename(
spec.getMainImage().getUsername(),
spec.getMainImage().getFilename(),
modifier
);
}
recipe.setMainImage(mainImage);
recipe.setModified(OffsetDateTime.now());
return this.recipeRepository.save(recipe); return this.recipeRepository.save(recipe);
} }
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe addViewer(long id, User modifier, User viewer) throws RecipeException { public Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException {
final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException( final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
)); ));
final Set<User> viewers = new HashSet<>(entity.getViewers()); final Set<User> viewers = new HashSet<>(entity.getViewers());
viewers.add((User) viewer); viewers.add(viewer);
entity.setViewers(viewers); entity.setViewers(viewers);
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
} }
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException { public Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException {
final Recipe entity = this.findRecipeEntity(id); final Recipe entity = this.findRecipeEntity(id);
final Set<User> viewers = new HashSet<>(entity.getViewers()); final Set<User> viewers = new HashSet<>(entity.getViewers());
viewers.remove((User) viewer); viewers.remove(viewer);
entity.setViewers(viewers); entity.setViewers(viewers);
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
} }
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe clearAllViewers(long id, User modifier) throws RecipeException { public Recipe clearAllViewers(Integer id, User modifier) throws RecipeException {
final Recipe entity = this.findRecipeEntity(id); final Recipe entity = this.findRecipeEntity(id);
entity.setViewers(new HashSet<>()); entity.setViewers(new HashSet<>());
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
@ -278,18 +302,18 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public void deleteRecipe(long id, User modifier) { public void deleteRecipe(Integer id, User modifier) {
this.recipeRepository.deleteById(id); this.recipeRepository.deleteById(id);
} }
@Override @Override
public FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer) { public FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
return this.getFullView((Recipe) recipe, includeRawText, viewer); return this.getFullView(recipe, includeRawText, viewer);
} }
@Override @Override
public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) { public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) {
return this.getInfoView((Recipe) recipe, viewer); return this.getInfoView(recipe, viewer);
} }
@Override @Override

View File

@ -0,0 +1,8 @@
package app.mealsmadeeasy.api.recipe.body;
import lombok.Data;
@Data
public class RecipeAiSearchBody {
private String prompt;
}

View File

@ -1,7 +1,10 @@
package app.mealsmadeeasy.api.recipe.body; package app.mealsmadeeasy.api.recipe.body;
import lombok.Data;
import java.util.Map; import java.util.Map;
@Data
public class RecipeSearchBody { public class RecipeSearchBody {
public enum Type { public enum Type {
@ -11,20 +14,4 @@ public class RecipeSearchBody {
private Type type; private Type type;
private Map<String, Object> data; private Map<String, Object> data;
public Type getType() {
return this.type;
}
public void setType(Type type) {
this.type = type;
}
public Map<String, Object> getData() {
return this.data;
}
public void setData(Map<String, Object> data) {
this.data = data;
}
} }

View File

@ -0,0 +1,23 @@
package app.mealsmadeeasy.api.recipe.body;
import lombok.Data;
import org.jetbrains.annotations.Nullable;
@Data
public class RecipeUpdateBody {
@Data
public static class MainImageUpdateBody {
private String username;
private String filename;
}
private @Nullable String title;
private @Nullable Integer preparationTime;
private @Nullable Integer cookingTime;
private @Nullable Integer totalTime;
private @Nullable String rawText;
private @Nullable Boolean isPublic;
private @Nullable MainImageUpdateBody mainImage;
}

View File

@ -8,6 +8,7 @@ import lombok.Data;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@Entity @Entity
@Table(name = "recipe_comment")
@Data @Data
public final class RecipeComment { public final class RecipeComment {

View File

@ -1,15 +1,8 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import lombok.Data;
@Data
public class RecipeCommentCreateBody { public class RecipeCommentCreateBody {
private String text; private String text;
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
} }

View File

@ -5,7 +5,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
public interface RecipeCommentRepository extends JpaRepository<RecipeComment, Long> { public interface RecipeCommentRepository extends JpaRepository<RecipeComment, Integer> {
void deleteAllByRecipe(Recipe recipe); void deleteAllByRecipe(Recipe recipe);
Slice<RecipeComment> findAllByRecipe(Recipe recipe, Pageable pageable); Slice<RecipeComment> findAllByRecipe(Recipe recipe, Pageable pageable);
} }

View File

@ -8,9 +8,9 @@ import org.springframework.data.domain.Slice;
public interface RecipeCommentService { public interface RecipeCommentService {
RecipeComment create(String recipeUsername, String recipeSlug, User owner, RecipeCommentCreateBody body) RecipeComment create(String recipeUsername, String recipeSlug, User owner, RecipeCommentCreateBody body)
throws RecipeException; throws RecipeException;
RecipeComment get(long commentId, User viewer) throws RecipeException; RecipeComment get(Integer commentId, User viewer) throws RecipeException;
Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer) Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer)
throws RecipeException; throws RecipeException;
RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException; RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException;
void delete(long commentId, User modifier) throws RecipeException; void delete(Integer commentId, User modifier) throws RecipeException;
} }

View File

@ -56,14 +56,14 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
} }
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)")
private RecipeComment loadCommentEntity(long commentId, User viewer) throws RecipeException { private RecipeComment loadCommentEntity(Integer commentId, User viewer) throws RecipeException {
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException( return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
)); ));
} }
@Override @Override
public RecipeComment get(long commentId, User viewer) throws RecipeException { public RecipeComment get(Integer commentId, User viewer) throws RecipeException {
return this.loadCommentEntity(commentId, viewer); return this.loadCommentEntity(commentId, viewer);
} }
@ -84,21 +84,21 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
} }
@Override @Override
public RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException { public RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException {
final RecipeComment entity = this.loadCommentEntity(commentId, viewer); final RecipeComment entity = this.loadCommentEntity(commentId, viewer);
entity.setRawText(spec.getRawText()); entity.setRawText(spec.getRawText());
return this.recipeCommentRepository.save(entity); return this.recipeCommentRepository.save(entity);
} }
@PostAuthorize("@recipeSecurity.isOwner(returnObject.recipe, #modifier)") @PostAuthorize("@recipeSecurity.isOwner(returnObject.recipe, #modifier)")
private RecipeComment loadForDelete(long commentId, User modifier) throws RecipeException { private RecipeComment loadForDelete(Integer commentId, User modifier) throws RecipeException {
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException( return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
)); ));
} }
@Override @Override
public void delete(long commentId, User modifier) throws RecipeException { public void delete(Integer commentId, User modifier) throws RecipeException {
final RecipeComment entityToDelete = this.loadForDelete(commentId, modifier); final RecipeComment entityToDelete = this.loadForDelete(commentId, modifier);
this.recipeCommentRepository.delete(entityToDelete); this.recipeCommentRepository.delete(entityToDelete);
} }

View File

@ -1,15 +1,10 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class RecipeCommentUpdateSpec { public class RecipeCommentUpdateSpec {
String rawText;
private String rawText;
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
} }

View File

@ -1,88 +1,36 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@Value
@Builder
public class RecipeCommentView { public class RecipeCommentView {
public static RecipeCommentView from(RecipeComment comment, boolean includeRawText) { public static RecipeCommentView from(RecipeComment comment, boolean includeRawText) {
final RecipeCommentView view = new RecipeCommentView(); final var builder = RecipeCommentView.builder()
view.setId(comment.getId()); .id(comment.getId())
view.setCreated(comment.getCreated()); .created(comment.getCreated())
view.setModified(comment.getModified()); .modified(comment.getModified())
view.setText(((RecipeComment) comment).getCachedRenderedText()); .text(comment.getCachedRenderedText())
.owner(UserInfoView.from(comment.getOwner()))
.recipeId(comment.getRecipe().getId());
if (includeRawText) { if (includeRawText) {
view.setRawText(comment.getRawText()); builder.rawText(comment.getRawText());
} }
view.setOwner(UserInfoView.from(comment.getOwner())); return builder.build();
view.setRecipeId(comment.getRecipe().getId());
return view;
} }
private Integer id; Integer id;
private OffsetDateTime created; OffsetDateTime created;
private @Nullable OffsetDateTime modified; @Nullable OffsetDateTime modified;
private String text; String text;
private @Nullable String rawText; @Nullable String rawText;
private UserInfoView owner; UserInfoView owner;
private Integer recipeId; Integer recipeId;
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
public @Nullable String getRawText() {
return this.rawText;
}
public void setRawText(@Nullable String rawText) {
this.rawText = rawText;
}
public UserInfoView getOwner() {
return this.owner;
}
public void setOwner(UserInfoView owner) {
this.owner = owner;
}
public Integer getRecipeId() {
return this.recipeId;
}
public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId;
}
} }

View File

@ -1,15 +0,0 @@
package app.mealsmadeeasy.api.recipe.spec;
public class RecipeAiSearchSpec {
private String prompt;
public String getPrompt() {
return this.prompt;
}
public void setPrompt(String prompt) {
this.prompt = prompt;
}
}

View File

@ -1,81 +1,19 @@
package app.mealsmadeeasy.api.recipe.spec; package app.mealsmadeeasy.api.recipe.spec;
import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.Image;
import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@Value
@Builder
public class RecipeCreateSpec { public class RecipeCreateSpec {
String slug;
private String slug; String title;
private String title; @Nullable Integer preparationTime;
private @Nullable Integer preparationTime; @Nullable Integer cookingTime;
private @Nullable Integer cookingTime; @Nullable Integer totalTime;
private @Nullable Integer totalTime; String rawText;
private String rawText; boolean isPublic;
private boolean isPublic; @Nullable Image mainImage;
private @Nullable Image mainImage;
public String getSlug() {
return this.slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
public boolean isPublic() {
return this.isPublic;
}
public void setPublic(boolean isPublic) {
this.isPublic = isPublic;
}
public @Nullable Image getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable Image mainImage) {
this.mainImage = mainImage;
}
} }

View File

@ -2,120 +2,73 @@ 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.Value;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
// 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.
@Value
@Builder
public class RecipeUpdateSpec { public class RecipeUpdateSpec {
@Value
@Builder
public static class MainImageUpdateSpec { public static class MainImageUpdateSpec {
String username;
private String username; String filename;
private String filename;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFilename() {
return this.filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
} }
private String title; public static RecipeUpdateSpec from(RecipeUpdateBody body) {
private @Nullable Integer preparationTime; final var b = RecipeUpdateSpec.builder()
private @Nullable Integer cookingTime; .title(body.getTitle())
private @Nullable Integer totalTime; .preparationTime(body.getPreparationTime())
private String rawText; .cookingTime(body.getCookingTime())
private boolean isPublic; .totalTime(body.getTotalTime())
private @Nullable MainImageUpdateSpec mainImage; .rawText(body.getRawText())
.isPublic(body.getIsPublic());
public RecipeUpdateSpec() {} final @Nullable RecipeUpdateBody.MainImageUpdateBody mainImage = body.getMainImage();
/**
* Convenience constructor for testing purposes.
*
* @param recipe the Recipe to copy from
*/
public RecipeUpdateSpec(Recipe recipe) {
this.title = recipe.getTitle();
this.preparationTime = recipe.getPreparationTime();
this.cookingTime = recipe.getCookingTime();
this.totalTime = recipe.getTotalTime();
this.rawText = recipe.getRawText();
this.isPublic = recipe.getIsPublic();
final @Nullable Image mainImage = recipe.getMainImage();
if (mainImage != null) { if (mainImage != null) {
this.mainImage = new MainImageUpdateSpec(); b.mainImage(
this.mainImage.setUsername(mainImage.getOwner().getUsername()); MainImageUpdateSpec.builder()
this.mainImage.setFilename(mainImage.getUserFilename()); .username(mainImage.getUsername())
.filename(mainImage.getFilename())
.build()
);
} }
return b.build();
} }
public @Nullable String getTitle() { // For testing convenience only.
return this.title; @ApiStatus.Internal
public static RecipeUpdateSpec.RecipeUpdateSpecBuilder fromRecipeToBuilder(Recipe recipe) {
final var b = RecipeUpdateSpec.builder()
.title(recipe.getTitle())
.preparationTime(recipe.getPreparationTime())
.cookingTime(recipe.getCookingTime())
.totalTime(recipe.getTotalTime())
.rawText(recipe.getRawText())
.isPublic(recipe.getIsPublic());
final @Nullable Image mainImage = recipe.getMainImage();
if (recipe.getMainImage() != null) {
b.mainImage(MainImageUpdateSpec.builder()
.username(mainImage.getOwner().getUsername())
.filename(mainImage.getUserFilename())
.build()
);
}
return b;
} }
public void setTitle(@Nullable String title) { String title;
this.title = title; @Nullable Integer preparationTime;
} @Nullable Integer cookingTime;
@Nullable Integer totalTime;
public @Nullable Integer getPreparationTime() { String rawText;
return this.preparationTime; Boolean isPublic;
} @Nullable MainImageUpdateSpec mainImage;
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public @Nullable String getRawText() {
return this.rawText;
}
public void setRawText(@Nullable String rawText) {
this.rawText = rawText;
}
public boolean getIsPublic() {
return this.isPublic;
}
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
public @Nullable MainImageUpdateSpec getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable MainImageUpdateSpec mainImage) {
this.mainImage = mainImage;
}
} }

View File

@ -8,7 +8,7 @@ import lombok.Data;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@Entity(name = "RecipeStar") @Entity
@Table(name = "recipe_star") @Table(name = "recipe_star")
@Data @Data
public final class RecipeStar { public final class RecipeStar {

View File

@ -2,10 +2,10 @@ package app.mealsmadeeasy.api.recipe.star;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Embeddable; import jakarta.persistence.Embeddable;
import lombok.Data;
import java.util.Objects;
@Embeddable @Embeddable
@Data
public class RecipeStarId { public class RecipeStarId {
@Column(name = "owner_id", nullable = false) @Column(name = "owner_id", nullable = false)
@ -14,39 +14,4 @@ public class RecipeStarId {
@Column(name = "recipe_id", nullable = false) @Column(name = "recipe_id", nullable = false)
private Integer recipeId; private Integer recipeId;
public Integer getOwnerId() {
return this.ownerId;
}
public void getOwnerId(Integer ownerId) {
this.ownerId = ownerId;
}
public Integer getRecipeId() {
return this.recipeId;
}
public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof RecipeStarId other) {
return this.recipeId.equals(other.recipeId) && this.ownerId.equals(other.ownerId);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.recipeId, this.ownerId);
}
@Override
public String toString() {
return "RecipeStarId(" + this.recipeId + ", " + this.ownerId + ")";
}
} }

View File

@ -25,7 +25,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
final RecipeStar draft = new RecipeStar(); final RecipeStar draft = new RecipeStar();
final RecipeStarId id = new RecipeStarId(); final RecipeStarId id = new RecipeStarId();
id.setRecipeId(recipeId); id.setRecipeId(recipeId);
id.getOwnerId(ownerId); id.setOwnerId(ownerId);
draft.setId(id); draft.setId(id);
draft.setTimestamp(OffsetDateTime.now()); draft.setTimestamp(OffsetDateTime.now());
return this.recipeStarRepository.save(draft); return this.recipeStarRepository.save(draft);

View File

@ -4,11 +4,14 @@ import app.mealsmadeeasy.api.image.view.ImageView;
import app.mealsmadeeasy.api.recipe.Recipe; 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 com.fasterxml.jackson.annotation.JsonInclude.Include; import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@Value
@Builder
public class FullRecipeView { public class FullRecipeView {
public static FullRecipeView from( public static FullRecipeView from(
@ -19,162 +22,46 @@ public class FullRecipeView {
int viewerCount, int viewerCount,
@Nullable ImageView mainImage @Nullable ImageView mainImage
) { ) {
final FullRecipeView view = new FullRecipeView(); final var b = FullRecipeView.builder()
view.setId(recipe.getId()); .id(recipe.getId())
view.setCreated(recipe.getCreated()); .created(recipe.getCreated())
view.setModified(recipe.getModified()); .modified(recipe.getModified())
view.setSlug(recipe.getSlug()); .slug(recipe.getSlug())
view.setTitle(recipe.getTitle()); .title(recipe.getTitle())
view.setPreparationTime(recipe.getPreparationTime()); .preparationTime(recipe.getPreparationTime())
view.setCookingTime(recipe.getCookingTime()); .cookingTime(recipe.getCookingTime())
view.setTotalTime(recipe.getTotalTime()); .totalTime(recipe.getTotalTime())
view.setText(renderedText); .text(renderedText)
.owner(UserInfoView.from(recipe.getOwner()))
.starCount(starCount)
.viewerCount(viewerCount)
.mainImage(mainImage)
.isPublic(recipe.getIsPublic());
if (includeRawText) { if (includeRawText) {
view.setRawText(recipe.getRawText()); b.rawText(recipe.getRawText());
} }
view.setOwner(UserInfoView.from(recipe.getOwner())); return b.build();
view.setStarCount(starCount);
view.setViewerCount(viewerCount);
view.setMainImage(mainImage);
view.setIsPublic(recipe.getIsPublic());
return view;
} }
private long id; Integer id;
private OffsetDateTime created; OffsetDateTime created;
private @Nullable OffsetDateTime modified; @Nullable OffsetDateTime modified;
private String slug; String slug;
private String title; String title;
private @Nullable Integer preparationTime; @Nullable Integer preparationTime;
private @Nullable Integer cookingTime; @Nullable Integer cookingTime;
private @Nullable Integer totalTime; @Nullable Integer totalTime;
private String text; String text;
private @Nullable String rawText; @Nullable String rawText;
private UserInfoView owner; UserInfoView owner;
private int starCount; int starCount;
private int viewerCount; int viewerCount;
private @Nullable ImageView mainImage; @Nullable ImageView mainImage;
private boolean isPublic; boolean isPublic;
public long getId() { @JsonInclude(JsonInclude.Include.NON_NULL)
return this.id;
}
public void setId(long id) {
this.id = id;
}
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}
public String getSlug() {
return this.slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
public @Nullable String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
@JsonInclude(Include.NON_NULL)
public @Nullable String getRawText() { public @Nullable String getRawText() {
return this.rawText; return this.rawText;
} }
public void setRawText(@Nullable String rawText) {
this.rawText = rawText;
}
public UserInfoView getOwner() {
return this.owner;
}
public void setOwner(UserInfoView owner) {
this.owner = owner;
}
public int getStarCount() {
return this.starCount;
}
public void setStarCount(int starCount) {
this.starCount = starCount;
}
public int getViewerCount() {
return this.viewerCount;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
public @Nullable ImageView getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable ImageView mainImage) {
this.mainImage = mainImage;
}
public boolean getIsPublic() {
return this.isPublic;
}
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
} }

View File

@ -1,5 +1,8 @@
package app.mealsmadeeasy.api.recipe.view; package app.mealsmadeeasy.api.recipe.view;
import lombok.Getter;
@Getter
public final class RecipeExceptionView { public final class RecipeExceptionView {
private final String type; private final String type;
@ -10,12 +13,4 @@ public final class RecipeExceptionView {
this.message = message; this.message = message;
} }
public String getType() {
return this.type;
}
public String getMessage() {
return this.message;
}
} }

View File

@ -3,42 +3,44 @@ 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.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView; import app.mealsmadeeasy.api.user.view.UserInfoView;
import lombok.Data; import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@Data @Value
@Builder
public class RecipeInfoView { public class RecipeInfoView {
public static RecipeInfoView from(Recipe recipe, int starCount, @Nullable ImageView mainImage) { public static RecipeInfoView from(Recipe recipe, int starCount, @Nullable ImageView mainImage) {
final RecipeInfoView view = new RecipeInfoView(); return RecipeInfoView.builder()
view.setId(recipe.getId()); .id(recipe.getId())
view.setCreated(recipe.getCreated()); .created(recipe.getCreated())
view.setModified(recipe.getModified()); .modified(recipe.getModified())
view.setSlug(recipe.getSlug()); .slug(recipe.getSlug())
view.setTitle(recipe.getTitle()); .title(recipe.getTitle())
view.setPreparationTime(recipe.getPreparationTime()); .preparationTime(recipe.getPreparationTime())
view.setCookingTime(recipe.getCookingTime()); .cookingTime(recipe.getCookingTime())
view.setTotalTime(recipe.getTotalTime()); .totalTime(recipe.getTotalTime())
view.setOwner(UserInfoView.from(recipe.getOwner())); .owner(UserInfoView.from(recipe.getOwner()))
view.setPublic(recipe.getIsPublic()); .isPublic(recipe.getIsPublic())
view.setStarCount(starCount); .starCount(starCount)
view.setMainImage(mainImage); .mainImage(mainImage)
return view; .build();
} }
private Integer id; Integer id;
private OffsetDateTime created; OffsetDateTime created;
private OffsetDateTime modified; OffsetDateTime modified;
private String slug; String slug;
private String title; String title;
private @Nullable Integer preparationTime; @Nullable Integer preparationTime;
private @Nullable Integer cookingTime; @Nullable Integer cookingTime;
private @Nullable Integer totalTime; @Nullable Integer totalTime;
private UserInfoView owner; UserInfoView owner;
private boolean isPublic; boolean isPublic;
private int starCount; int starCount;
private @Nullable ImageView mainImage; @Nullable ImageView mainImage;
} }