Remove recipe-related interfaces and replace with entities.

This commit is contained in:
Jesse Brault 2026-01-14 16:44:13 -06:00
parent 1f256b84bc
commit 7230c7887d
26 changed files with 221 additions and 610 deletions

View File

@ -29,6 +29,10 @@ sourceSets {
} }
configurations { configurations {
compileOnly {
extendsFrom annotationProcessor
}
testFixturesImplementation { testFixturesImplementation {
extendsFrom implementation extendsFrom implementation
} }
@ -60,6 +64,8 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// flyway // flyway
implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-core'

Binary file not shown.

View File

@ -37,57 +37,57 @@ public class RecipeRepositoryTests {
@Test @Test
public void findsAllPublicRecipes() { public void findsAllPublicRecipes() {
final RecipeEntity publicRecipe = new RecipeEntity(); final Recipe publicRecipe = new Recipe();
publicRecipe.setCreated(OffsetDateTime.now()); publicRecipe.setCreated(OffsetDateTime.now());
publicRecipe.setSlug(UUID.randomUUID().toString()); publicRecipe.setSlug(UUID.randomUUID().toString());
publicRecipe.setPublic(true); publicRecipe.setIsPublic(true);
publicRecipe.setOwner(this.seedUser()); publicRecipe.setOwner(this.seedUser());
publicRecipe.setTitle("Public Recipe"); publicRecipe.setTitle("Public Recipe");
publicRecipe.setRawText("Hello, World!"); publicRecipe.setRawText("Hello, World!");
final RecipeEntity savedRecipe = this.recipeRepository.save(publicRecipe); final Recipe savedRecipe = this.recipeRepository.save(publicRecipe);
final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
assertThat(publicRecipes).anyMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId())); assertThat(publicRecipes).anyMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
} }
@Test @Test
public void doesNotFindNonPublicRecipe() { public void doesNotFindNonPublicRecipe() {
final RecipeEntity nonPublicRecipe = new RecipeEntity(); final Recipe nonPublicRecipe = new Recipe();
nonPublicRecipe.setCreated(OffsetDateTime.now()); nonPublicRecipe.setCreated(OffsetDateTime.now());
nonPublicRecipe.setSlug(UUID.randomUUID().toString()); nonPublicRecipe.setSlug(UUID.randomUUID().toString());
nonPublicRecipe.setOwner(this.seedUser()); nonPublicRecipe.setOwner(this.seedUser());
nonPublicRecipe.setTitle("Non-Public Recipe"); nonPublicRecipe.setTitle("Non-Public Recipe");
nonPublicRecipe.setRawText("Hello, World!"); nonPublicRecipe.setRawText("Hello, World!");
final RecipeEntity savedRecipe = this.recipeRepository.save(nonPublicRecipe); final Recipe savedRecipe = this.recipeRepository.save(nonPublicRecipe);
final List<RecipeEntity> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(); final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
assertThat(publicRecipes).noneMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId())); assertThat(publicRecipes).noneMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
} }
@Test @Test
public void findsAllForViewer() { public void findsAllForViewer() {
final RecipeEntity recipe = new RecipeEntity(); final Recipe recipe = new Recipe();
recipe.setCreated(OffsetDateTime.now()); recipe.setCreated(OffsetDateTime.now());
recipe.setSlug(UUID.randomUUID().toString()); recipe.setSlug(UUID.randomUUID().toString());
recipe.setOwner(this.seedUser()); recipe.setOwner(this.seedUser());
recipe.setTitle("Test Recipe"); recipe.setTitle("Test Recipe");
recipe.setRawText("Hello, World!"); recipe.setRawText("Hello, World!");
final RecipeEntity saved = this.recipeRepository.save(recipe); final Recipe saved = this.recipeRepository.save(recipe);
final UserEntity viewer = this.seedUser(); final UserEntity viewer = this.seedUser();
final Set<UserEntity> viewers = new HashSet<>(recipe.getViewerEntities()); final Set<UserEntity> viewers = new HashSet<>(recipe.getViewers());
viewers.add(viewer); viewers.add(viewer);
saved.setViewers(viewers); saved.setViewers(viewers);
this.recipeRepository.save(saved); this.recipeRepository.save(saved);
final List<RecipeEntity> viewable = this.recipeRepository.findAllByViewersContaining(viewer); final List<Recipe> viewable = this.recipeRepository.findAllByViewersContaining(viewer);
assertThat(viewable.size()).isEqualTo(1); assertThat(viewable.size()).isEqualTo(1);
} }
@Test @Test
public void doesNotIncludeNonViewable() { public void doesNotIncludeNonViewable() {
final RecipeEntity recipe = new RecipeEntity(); final Recipe recipe = new Recipe();
recipe.setCreated(OffsetDateTime.now()); recipe.setCreated(OffsetDateTime.now());
recipe.setSlug(UUID.randomUUID().toString()); recipe.setSlug(UUID.randomUUID().toString());
recipe.setOwner(this.seedUser()); recipe.setOwner(this.seedUser());
@ -96,7 +96,7 @@ public class RecipeRepositoryTests {
this.recipeRepository.save(recipe); this.recipeRepository.save(recipe);
final UserEntity viewer = this.seedUser(); final UserEntity viewer = this.seedUser();
final List<RecipeEntity> viewable = this.recipeRepository.findAllByViewersContaining(viewer); final List<Recipe> viewable = this.recipeRepository.findAllByViewersContaining(viewer);
assertThat(viewable.size()).isEqualTo(0); assertThat(viewable.size()).isEqualTo(0);
} }

View File

@ -100,7 +100,7 @@ public class RecipeServiceTests {
assertThat(byId.getSlug(), is(recipe.getSlug())); assertThat(byId.getSlug(), is(recipe.getSlug()));
assertThat(byId.getTitle(), is("My Recipe")); assertThat(byId.getTitle(), is("My Recipe"));
assertThat(byId.getRawText(), is("Hello!")); assertThat(byId.getRawText(), is("Hello!"));
assertThat(byId.isPublic(), is(true)); assertThat(byId.getIsPublic(), is(true));
} }
@Test @Test

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.IntegrationTestsExtension; import app.mealsmadeeasy.api.IntegrationTestsExtension;
import app.mealsmadeeasy.api.recipe.RecipeEntity; import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeRepository; import app.mealsmadeeasy.api.recipe.RecipeRepository;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository; import app.mealsmadeeasy.api.user.UserRepository;
@ -38,8 +38,8 @@ public class RecipeStarRepositoryTests {
return this.userRepository.save(draft); return this.userRepository.save(draft);
} }
private RecipeEntity getTestRecipe(UserEntity owner) { private Recipe getTestRecipe(UserEntity owner) {
final RecipeEntity recipeDraft = new RecipeEntity(); final Recipe recipeDraft = new Recipe();
recipeDraft.setCreated(OffsetDateTime.now()); recipeDraft.setCreated(OffsetDateTime.now());
recipeDraft.setSlug(UUID.randomUUID().toString()); recipeDraft.setSlug(UUID.randomUUID().toString());
recipeDraft.setOwner(owner); recipeDraft.setOwner(owner);
@ -51,9 +51,9 @@ public class RecipeStarRepositoryTests {
@Test @Test
public void returnsTrueIfStarer() { public void returnsTrueIfStarer() {
final UserEntity owner = this.seedUser(); final UserEntity owner = this.seedUser();
final RecipeEntity recipe = this.getTestRecipe(owner); final Recipe recipe = this.getTestRecipe(owner);
final RecipeStarEntity starDraft = new RecipeStarEntity(); 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.getOwnerId(owner.getId());
@ -73,7 +73,7 @@ public class RecipeStarRepositoryTests {
@Test @Test
public void returnsFalseIfNotStarer() { public void returnsFalseIfNotStarer() {
final UserEntity owner = this.seedUser(); final UserEntity owner = this.seedUser();
final RecipeEntity recipe = this.getTestRecipe(owner); final Recipe recipe = this.getTestRecipe(owner);
assertThat( assertThat(
this.recipeStarRepository.isStarer( this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api; package app.mealsmadeeasy.api;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeEmbeddingEntity; import app.mealsmadeeasy.api.recipe.RecipeEmbeddingEntity;
import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.recipe.RecipeRepository; import app.mealsmadeeasy.api.recipe.RecipeRepository;
import app.mealsmadeeasy.api.recipe.RecipeService; import app.mealsmadeeasy.api.recipe.RecipeService;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -37,20 +37,20 @@ public class BackfillRecipeEmbeddings implements ApplicationRunner {
@Override @Override
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) {
final List<RecipeEntity> recipeEntities = this.recipeRepository.findAllByEmbeddingIsNull(); final List<Recipe> recipeEntities = this.recipeRepository.findAllByEmbeddingIsNull();
for (final RecipeEntity recipeEntity : recipeEntities) { for (final Recipe recipe : recipeEntities) {
logger.info("Calculating embedding for {}", recipeEntity); logger.info("Calculating embedding for {}", recipe);
final String renderedMarkdown = this.recipeService.getRenderedMarkdown(recipeEntity); final String renderedMarkdown = this.recipeService.getRenderedMarkdown(recipe);
final String toEmbed = "<h1>" + recipeEntity.getTitle() + "</h1>" + renderedMarkdown; final String toEmbed = "<h1>" + recipe.getTitle() + "</h1>" + renderedMarkdown;
final float[] embedding = this.embeddingModel.embed(toEmbed); final float[] embedding = this.embeddingModel.embed(toEmbed);
final RecipeEmbeddingEntity recipeEmbedding = new RecipeEmbeddingEntity(); final RecipeEmbeddingEntity recipeEmbedding = new RecipeEmbeddingEntity();
recipeEmbedding.setRecipe(recipeEntity); recipeEmbedding.setRecipe(recipe);
recipeEmbedding.setEmbedding(embedding); recipeEmbedding.setEmbedding(embedding);
recipeEmbedding.setTimestamp(OffsetDateTime.now()); recipeEmbedding.setTimestamp(OffsetDateTime.now());
recipeEntity.setEmbedding(recipeEmbedding); recipe.setEmbedding(recipeEmbedding);
this.recipeRepository.save(recipeEntity); this.recipeRepository.save(recipe);
} }
this.recipeRepository.flush(); this.recipeRepository.flush();
} }

View File

@ -1,28 +1,83 @@
package app.mealsmadeeasy.api.recipe; package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.Image; import app.mealsmadeeasy.api.image.S3ImageEntity;
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.UserEntity;
import jakarta.persistence.*;
import lombok.Data;
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.Set; import java.util.Set;
public interface Recipe { @Entity(name = "Recipe")
Integer getId(); @Data
OffsetDateTime getCreated(); public final class Recipe {
@Nullable OffsetDateTime getModified();
String getSlug(); @Id
String getTitle(); @GeneratedValue(strategy = GenerationType.IDENTITY)
@Nullable Integer getPreparationTime(); @Column(nullable = false, updatable = false)
@Nullable Integer getCookingTime(); private Integer id;
@Nullable Integer getTotalTime();
String getRawText(); @Column(nullable = false)
User getOwner(); private OffsetDateTime created;
Set<RecipeStar> getStars();
boolean isPublic(); private OffsetDateTime modified;
Set<User> getViewers();
Set<RecipeComment> getComments(); @Column(nullable = false, unique = true)
@Nullable Image getMainImage(); private String slug;
@Column(nullable = false)
private String title;
@Nullable
private Integer preparationTime;
@Nullable
private Integer cookingTime;
@Nullable
private Integer totalTime;
@Lob
@Column(name = "raw_text", columnDefinition = "TEXT", nullable = false)
@Basic(fetch = FetchType.LAZY)
private String rawText;
@Lob
@Column(name = "cached_rendered_text", columnDefinition = "TEXT")
@Basic(fetch = FetchType.LAZY)
private String cachedRenderedText;
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
private UserEntity owner;
@OneToMany
@JoinColumn(name = "recipe_id")
private Set<RecipeStar> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe")
private Set<RecipeComment> comments = new HashSet<>();
@Column(nullable = false)
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "recipe_viewer",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne
@JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage;
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private RecipeEmbeddingEntity embedding;
} }

View File

@ -18,7 +18,7 @@ public class RecipeEmbeddingEntity {
@OneToOne(fetch = FetchType.LAZY, optional = false) @OneToOne(fetch = FetchType.LAZY, optional = false)
@MapsId @MapsId
@JoinColumn(name = "recipe_id") @JoinColumn(name = "recipe_id")
private RecipeEntity recipe; private Recipe recipe;
@JdbcTypeCode(SqlTypes.VECTOR) @JdbcTypeCode(SqlTypes.VECTOR)
@Array(length = 1024) @Array(length = 1024)
@ -36,11 +36,11 @@ public class RecipeEmbeddingEntity {
this.id = id; this.id = id;
} }
public RecipeEntity getRecipe() { public Recipe getRecipe() {
return this.recipe; return this.recipe;
} }
public void setRecipe(RecipeEntity recipe) { public void setRecipe(Recipe recipe) {
this.recipe = recipe; this.recipe = recipe;
} }

View File

@ -1,252 +0,0 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.S3ImageEntity;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity(name = "Recipe")
public final class RecipeEntity implements Recipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Integer id;
@Column(nullable = false)
private OffsetDateTime created;
private OffsetDateTime modified;
@Column(nullable = false, unique = true)
private String slug;
@Column(nullable = false)
private String title;
@Nullable
private Integer preparationTime;
@Nullable
private Integer cookingTime;
@Nullable
private Integer totalTime;
@Lob
@Column(name = "raw_text", columnDefinition = "TEXT", nullable = false)
@Basic(fetch = FetchType.LAZY)
private String rawText;
@Lob
@Column(name = "cached_rendered_text", columnDefinition = "TEXT")
@Basic(fetch = FetchType.LAZY)
private String cachedRenderedText;
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
private UserEntity owner;
@OneToMany
@JoinColumn(name = "recipe_id")
private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe")
private Set<RecipeCommentEntity> comments = new HashSet<>();
@Column(nullable = false)
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "recipe_viewer",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne
@JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage;
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private RecipeEmbeddingEntity embedding;
@Override
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}
@Override
public String getSlug() {
return this.slug;
}
public void setSlug(String slug) {
this.slug = slug;
}
@Override
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public @Nullable Integer getPreparationTime() {
return this.preparationTime;
}
public void setPreparationTime(@Nullable Integer preparationTime) {
this.preparationTime = preparationTime;
}
@Override
public @Nullable Integer getCookingTime() {
return this.cookingTime;
}
public void setCookingTime(@Nullable Integer cookingTime) {
this.cookingTime = cookingTime;
}
@Override
public @Nullable Integer getTotalTime() {
return this.totalTime;
}
public void setTotalTime(@Nullable Integer totalTime) {
this.totalTime = totalTime;
}
@Override
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
public @Nullable String getCachedRenderedText() {
return this.cachedRenderedText;
}
public void setCachedRenderedText(@Nullable String cachedRenderedText) {
this.cachedRenderedText = cachedRenderedText;
}
@Override
public UserEntity getOwner() {
return this.owner;
}
public void setOwner(UserEntity owner) {
this.owner = owner;
}
@Override
public boolean isPublic() {
return this.isPublic;
}
public void setPublic(Boolean isPublic) {
this.isPublic = isPublic;
}
@Override
public Set<User> getViewers() {
return Set.copyOf(this.viewers);
}
public Set<UserEntity> getViewerEntities() {
return this.viewers;
}
public void setViewers(Set<UserEntity> viewers) {
this.viewers = viewers;
}
@Override
public Set<RecipeStar> getStars() {
return Set.copyOf(this.stars);
}
public Set<RecipeStarEntity> getStarEntities() {
return this.stars;
}
public void setStarEntities(Set<RecipeStarEntity> starGazers) {
this.stars = starGazers;
}
@Override
public Set<RecipeComment> getComments() {
return Set.copyOf(this.comments);
}
public Set<RecipeCommentEntity> getCommentEntities() {
return this.comments;
}
public void setComments(Set<RecipeCommentEntity> comments) {
this.comments = comments;
}
@Override
public String toString() {
return "RecipeEntity(" + this.id + ", " + this.title + ")";
}
@Override
public @Nullable S3ImageEntity getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable S3ImageEntity image) {
this.mainImage = image;
}
public RecipeEmbeddingEntity getEmbedding() {
return this.embedding;
}
public void setEmbedding(RecipeEmbeddingEntity embedding) {
this.embedding = embedding;
}
}

View File

@ -10,27 +10,27 @@ 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<RecipeEntity, Long> { public interface RecipeRepository extends JpaRepository<Recipe, Long> {
List<RecipeEntity> findAllByIsPublicIsTrue(); List<Recipe> findAllByIsPublicIsTrue();
List<RecipeEntity> findAllByViewersContaining(UserEntity viewer); List<Recipe> findAllByViewersContaining(UserEntity viewer);
List<RecipeEntity> findAllByOwner(UserEntity owner); List<Recipe> findAllByOwner(UserEntity owner);
@Query("SELECT r from Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2") @Query("SELECT r from Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2")
Optional<RecipeEntity> findByOwnerUsernameAndSlug(String ownerUsername, String slug); Optional<Recipe> findByOwnerUsernameAndSlug(String ownerUsername, String slug);
@Query("SELECT r FROM Recipe r WHERE size(r.stars) >= ?1 AND (r.isPublic OR ?2 MEMBER OF r.viewers)") @Query("SELECT r FROM Recipe r WHERE size(r.stars) >= ?1 AND (r.isPublic OR ?2 MEMBER OF r.viewers)")
List<RecipeEntity> findAllViewableByStarsGreaterThanEqual(long stars, UserEntity viewer); List<Recipe> findAllViewableByStarsGreaterThanEqual(long stars, UserEntity viewer);
@Query("SELECT r FROM Recipe r WHERE r.id = ?1") @Query("SELECT r FROM Recipe r WHERE r.id = ?1")
@EntityGraph(attributePaths = { "viewers" }) @EntityGraph(attributePaths = { "viewers" })
Optional<RecipeEntity> findByIdWithViewers(long id); Optional<Recipe> findByIdWithViewers(long id);
@Query("SELECT r FROM Recipe r WHERE r.id = ?1") @Query("SELECT r FROM Recipe r WHERE r.id = ?1")
@EntityGraph(attributePaths = { "stars" }) @EntityGraph(attributePaths = { "stars" })
Optional<RecipeEntity> findByIdWithStars(long id); Optional<Recipe> findByIdWithStars(long id);
@Query("SELECT size(r.stars) FROM Recipe r WHERE r.id = ?1") @Query("SELECT size(r.stars) FROM Recipe r WHERE r.id = ?1")
int getStarCount(long recipeId); int getStarCount(long recipeId);
@ -39,9 +39,9 @@ public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
int getViewerCount(long recipeId); int getViewerCount(long recipeId);
@Query("SELECT r FROM Recipe r WHERE r.isPublic OR r.owner = ?1 OR ?1 MEMBER OF r.viewers") @Query("SELECT r FROM Recipe r WHERE r.isPublic OR r.owner = ?1 OR ?1 MEMBER OF r.viewers")
Slice<RecipeEntity> findAllViewableBy(UserEntity viewer, Pageable pageable); Slice<Recipe> findAllViewableBy(UserEntity viewer, Pageable pageable);
List<RecipeEntity> findAllByEmbeddingIsNull(); List<Recipe> findAllByEmbeddingIsNull();
@Query( @Query(
nativeQuery = true, nativeQuery = true,
@ -57,7 +57,7 @@ public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
ORDER BY d.distance; ORDER BY d.distance;
""" """
) )
List<RecipeEntity> searchByEmbeddingAndViewableBy(float[] queryEmbedding, float similarity, Integer viewerId); List<Recipe> searchByEmbeddingAndViewableBy(float[] queryEmbedding, float similarity, Integer viewerId);
@Query( @Query(
nativeQuery = true, nativeQuery = true,
@ -69,6 +69,6 @@ public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
ORDER BY d.distance; ORDER BY d.distance;
""" """
) )
List<RecipeEntity> searchByEmbeddingAndIsPublic(float[] queryEmbedding, float similarity); List<Recipe> searchByEmbeddingAndIsPublic(float[] queryEmbedding, float similarity);
} }

View File

@ -42,7 +42,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
@Override @Override
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException { public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
if (recipe.isPublic()) { if (recipe.getIsPublic()) {
// public recipe // public recipe
return true; return true;
} else if (user == null) { } else if (user == null) {
@ -53,7 +53,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
return true; return true;
} else { } else {
// check if viewer // check if viewer
final RecipeEntity withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId()) final Recipe withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
.orElseThrow(() -> new RecipeException( .orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId() RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId()
)); ));

View File

@ -58,6 +58,6 @@ public interface RecipeService {
@Nullable Boolean isOwner(String username, String slug, @Nullable User viewer); @Nullable Boolean isOwner(String username, String slug, @Nullable User viewer);
@ApiStatus.Internal @ApiStatus.Internal
String getRenderedMarkdown(RecipeEntity entity); String getRenderedMarkdown(Recipe entity);
} }

View File

@ -58,18 +58,18 @@ public class RecipeServiceImpl implements RecipeService {
if (owner == null) { if (owner == null) {
throw new AccessDeniedException("Must be logged in."); throw new AccessDeniedException("Must be logged in.");
} }
final RecipeEntity draft = new RecipeEntity(); final Recipe draft = new Recipe();
draft.setCreated(OffsetDateTime.now()); draft.setCreated(OffsetDateTime.now());
draft.setOwner((UserEntity) owner); draft.setOwner((UserEntity) 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((S3ImageEntity) spec.getMainImage()); draft.setMainImage((S3ImageEntity) spec.getMainImage());
draft.setPublic(spec.isPublic()); draft.setIsPublic(spec.isPublic());
return this.recipeRepository.save(draft); return this.recipeRepository.save(draft);
} }
private RecipeEntity findRecipeEntity(long id) throws RecipeException { private Recipe findRecipeEntity(long 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
)); ));
@ -101,7 +101,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@ApiStatus.Internal @ApiStatus.Internal
public String getRenderedMarkdown(RecipeEntity entity) { public String getRenderedMarkdown(Recipe entity) {
if (entity.getCachedRenderedText() == null) { if (entity.getCachedRenderedText() == null) {
entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText())); entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText()));
entity = this.recipeRepository.save(entity); entity = this.recipeRepository.save(entity);
@ -126,7 +126,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
} }
private FullRecipeView getFullView(RecipeEntity recipe, boolean includeRawText, @Nullable User viewer) { private FullRecipeView getFullView(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
return FullRecipeView.from( return FullRecipeView.from(
recipe, recipe,
this.getRenderedMarkdown(recipe), this.getRenderedMarkdown(recipe),
@ -137,7 +137,7 @@ public class RecipeServiceImpl implements RecipeService {
); );
} }
private RecipeInfoView getInfoView(RecipeEntity recipe, @Nullable User viewer) { private RecipeInfoView getInfoView(Recipe recipe, @Nullable User viewer) {
return RecipeInfoView.from( return RecipeInfoView.from(
recipe, recipe,
this.getStarCount(recipe), this.getStarCount(recipe),
@ -148,7 +148,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(long id, @Nullable User viewer) throws RecipeException {
final RecipeEntity 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
)); ));
return this.getFullView(recipe, false, viewer); return this.getFullView(recipe, false, viewer);
@ -162,7 +162,7 @@ public class RecipeServiceImpl implements RecipeService {
boolean includeRawText, boolean includeRawText,
@Nullable User viewer @Nullable User viewer
) throws RecipeException { ) throws RecipeException {
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug) final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug)
.orElseThrow(() -> new RecipeException( .orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_USERNAME_OR_SLUG, RecipeException.Type.INVALID_USERNAME_OR_SLUG,
"No such Recipe for username " + username + " and slug: " + slug "No such Recipe for username " + username + " and slug: " + slug
@ -202,7 +202,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
public List<RecipeInfoView> aiSearch(RecipeAiSearchSpec searchSpec, @Nullable User viewer) { public List<RecipeInfoView> aiSearch(RecipeAiSearchSpec searchSpec, @Nullable User viewer) {
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt()); final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
final List<RecipeEntity> results; final List<Recipe> results;
if (viewer == null) { if (viewer == null) {
results = this.recipeRepository.searchByEmbeddingAndIsPublic(queryEmbedding, 0.5f); results = this.recipeRepository.searchByEmbeddingAndIsPublic(queryEmbedding, 0.5f);
} else { } else {
@ -217,7 +217,7 @@ public class RecipeServiceImpl implements RecipeService {
@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)
throws RecipeException, ImageException { throws RecipeException, ImageException {
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() -> final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() ->
new RecipeException( new RecipeException(
RecipeException.Type.INVALID_USERNAME_OR_SLUG, RecipeException.Type.INVALID_USERNAME_OR_SLUG,
"No such Recipe for username " + username + " and slug: " + slug "No such Recipe for username " + username + " and slug: " + slug
@ -230,7 +230,7 @@ public class RecipeServiceImpl implements RecipeService {
recipe.setTotalTime(spec.getTotalTime()); recipe.setTotalTime(spec.getTotalTime());
recipe.setRawText(spec.getRawText()); recipe.setRawText(spec.getRawText());
recipe.setCachedRenderedText(null); recipe.setCachedRenderedText(null);
recipe.setPublic(spec.getIsPublic()); recipe.setIsPublic(spec.getIsPublic());
final S3ImageEntity mainImage; final S3ImageEntity mainImage;
if (spec.getMainImage() == null) { if (spec.getMainImage() == null) {
@ -251,10 +251,10 @@ public class RecipeServiceImpl implements RecipeService {
@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(long id, User modifier, User viewer) throws RecipeException {
final RecipeEntity 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<UserEntity> viewers = new HashSet<>(entity.getViewerEntities()); final Set<UserEntity> viewers = new HashSet<>(entity.getViewers());
viewers.add((UserEntity) viewer); viewers.add((UserEntity) viewer);
entity.setViewers(viewers); entity.setViewers(viewers);
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
@ -263,8 +263,8 @@ public class RecipeServiceImpl implements RecipeService {
@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(long id, User modifier, User viewer) throws RecipeException {
final RecipeEntity entity = this.findRecipeEntity(id); final Recipe entity = this.findRecipeEntity(id);
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities()); final Set<UserEntity> viewers = new HashSet<>(entity.getViewers());
viewers.remove((UserEntity) viewer); viewers.remove((UserEntity) viewer);
entity.setViewers(viewers); entity.setViewers(viewers);
return this.recipeRepository.save(entity); return this.recipeRepository.save(entity);
@ -273,7 +273,7 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)") @PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe clearAllViewers(long id, User modifier) throws RecipeException { public Recipe clearAllViewers(long id, User modifier) throws RecipeException {
final RecipeEntity 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);
} }
@ -286,12 +286,12 @@ public class RecipeServiceImpl implements RecipeService {
@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((RecipeEntity) recipe, includeRawText, viewer); return this.getFullView((Recipe) recipe, includeRawText, viewer);
} }
@Override @Override
public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) { public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) {
return this.getInfoView((RecipeEntity) recipe, viewer); return this.getInfoView((Recipe) recipe, viewer);
} }
@Override @Override

View File

@ -1,16 +1,40 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.recipe.Recipe; import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.UserEntity;
import org.jetbrains.annotations.Nullable; import jakarta.persistence.*;
import lombok.Data;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
public interface RecipeComment { @Entity
Integer getId(); @Data
OffsetDateTime getCreated(); public final class RecipeComment {
@Nullable OffsetDateTime getModified();
String getRawText(); @Id
User getOwner(); @GeneratedValue(strategy = GenerationType.IDENTITY)
Recipe getRecipe(); @Column(nullable = false)
private Integer id;
@Column(nullable = false, updatable = false)
private OffsetDateTime created = OffsetDateTime.now();
private OffsetDateTime modified;
@Lob
@Basic(fetch = FetchType.LAZY)
private String rawText;
@Lob
@Basic(fetch = FetchType.LAZY)
private String cachedRenderedText;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false, updatable = false)
private UserEntity owner;
@ManyToOne
@JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private Recipe recipe;
} }

View File

@ -1,100 +0,0 @@
package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import java.time.OffsetDateTime;
@Entity(name = "RecipeComment")
@Table(name = "recipe_comment")
public final class RecipeCommentEntity implements RecipeComment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Integer id;
@Column(nullable = false, updatable = false)
private OffsetDateTime created = OffsetDateTime.now();
private OffsetDateTime modified;
@Lob
@Basic(fetch = FetchType.LAZY)
private String rawText;
@Lob
@Basic(fetch = FetchType.LAZY)
private String cachedRenderedText;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false, updatable = false)
private UserEntity owner;
@ManyToOne
@JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe;
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public OffsetDateTime getModified() {
return this.modified;
}
public void setModified(OffsetDateTime modified) {
this.modified = modified;
}
@Override
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
public String getCachedRenderedText() {
return this.cachedRenderedText;
}
public void setCachedRenderedText(String cachedRenderedText) {
this.cachedRenderedText = cachedRenderedText;
}
@Override
public UserEntity getOwner() {
return this.owner;
}
public void setOwner(UserEntity owner) {
this.owner = owner;
}
@Override
public RecipeEntity getRecipe() {
return this.recipe;
}
public void setRecipe(RecipeEntity recipe) {
this.recipe = recipe;
}
}

View File

@ -1,11 +1,11 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.recipe.RecipeEntity; import app.mealsmadeeasy.api.recipe.Recipe;
import org.springframework.data.domain.Pageable; 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<RecipeCommentEntity, Long> { public interface RecipeCommentRepository extends JpaRepository<RecipeComment, Long> {
void deleteAllByRecipe(RecipeEntity recipe); void deleteAllByRecipe(Recipe recipe);
Slice<RecipeCommentEntity> findAllByRecipe(RecipeEntity recipe, Pageable pageable); Slice<RecipeComment> findAllByRecipe(Recipe recipe, Pageable pageable);
} }

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe.comment; package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.markdown.MarkdownService; import app.mealsmadeeasy.api.markdown.MarkdownService;
import app.mealsmadeeasy.api.recipe.RecipeEntity; import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeException; import app.mealsmadeeasy.api.recipe.RecipeException;
import app.mealsmadeeasy.api.recipe.RecipeRepository; import app.mealsmadeeasy.api.recipe.RecipeRepository;
import app.mealsmadeeasy.api.user.User; import app.mealsmadeeasy.api.user.User;
@ -42,12 +42,12 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
RecipeCommentCreateBody body RecipeCommentCreateBody body
) throws RecipeException { ) throws RecipeException {
requireNonNull(commenter); requireNonNull(commenter);
final RecipeCommentEntity draft = new RecipeCommentEntity(); final RecipeComment draft = new RecipeComment();
draft.setCreated(OffsetDateTime.now()); draft.setCreated(OffsetDateTime.now());
draft.setRawText(body.getText()); draft.setRawText(body.getText());
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText())); draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
draft.setOwner((UserEntity) commenter); draft.setOwner((UserEntity) commenter);
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug) final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug)
.orElseThrow(() -> new RecipeException( .orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_USERNAME_OR_SLUG, RecipeException.Type.INVALID_USERNAME_OR_SLUG,
"Invalid username or slug: " + recipeUsername + "/" + recipeSlug "Invalid username or slug: " + recipeUsername + "/" + recipeSlug
@ -57,7 +57,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
} }
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)") @PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)")
private RecipeCommentEntity loadCommentEntity(long commentId, User viewer) throws RecipeException { private RecipeComment loadCommentEntity(long 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
)); ));
@ -71,13 +71,13 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
@Override @Override
@PreAuthorize("@recipeSecurity.isViewableBy(#recipeUsername, #recipeSlug, #viewer)") @PreAuthorize("@recipeSecurity.isViewableBy(#recipeUsername, #recipeSlug, #viewer)")
public Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer) throws RecipeException { public Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer) throws RecipeException {
final RecipeEntity recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug).orElseThrow( final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug).orElseThrow(
() -> new RecipeException( () -> new RecipeException(
RecipeException.Type.INVALID_USERNAME_OR_SLUG, RecipeException.Type.INVALID_USERNAME_OR_SLUG,
"No such Recipe for username/slug: " + recipeUsername + "/" + recipeSlug "No such Recipe for username/slug: " + recipeUsername + "/" + recipeSlug
) )
); );
final Slice<RecipeCommentEntity> commentEntities = this.recipeCommentRepository.findAllByRecipe(recipe, pageable); final Slice<RecipeComment> commentEntities = this.recipeCommentRepository.findAllByRecipe(recipe, pageable);
return commentEntities.map(commentEntity -> RecipeCommentView.from( return commentEntities.map(commentEntity -> RecipeCommentView.from(
commentEntity, commentEntity,
false false
@ -86,13 +86,13 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
@Override @Override
public RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException { public RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException {
final RecipeCommentEntity 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 RecipeCommentEntity loadForDelete(long commentId, User modifier) throws RecipeException { private RecipeComment loadForDelete(long 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
)); ));
@ -100,7 +100,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
@Override @Override
public void delete(long commentId, User modifier) throws RecipeException { public void delete(long commentId, User modifier) throws RecipeException {
final RecipeCommentEntity entityToDelete = this.loadForDelete(commentId, modifier); final RecipeComment entityToDelete = this.loadForDelete(commentId, modifier);
this.recipeCommentRepository.delete(entityToDelete); this.recipeCommentRepository.delete(entityToDelete);
} }

View File

@ -12,7 +12,7 @@ public class RecipeCommentView {
view.setId(comment.getId()); view.setId(comment.getId());
view.setCreated(comment.getCreated()); view.setCreated(comment.getCreated());
view.setModified(comment.getModified()); view.setModified(comment.getModified());
view.setText(((RecipeCommentEntity) comment).getCachedRenderedText()); view.setText(((RecipeComment) comment).getCachedRenderedText());
if (includeRawText) { if (includeRawText) {
view.setRawText(comment.getRawText()); view.setRawText(comment.getRawText());
} }

View File

@ -53,7 +53,7 @@ public class RecipeUpdateSpec {
this.cookingTime = recipe.getCookingTime(); this.cookingTime = recipe.getCookingTime();
this.totalTime = recipe.getTotalTime(); this.totalTime = recipe.getTotalTime();
this.rawText = recipe.getRawText(); this.rawText = recipe.getRawText();
this.isPublic = recipe.isPublic(); this.isPublic = recipe.getIsPublic();
final @Nullable Image mainImage = recipe.getMainImage(); final @Nullable Image mainImage = recipe.getMainImage();
if (mainImage != null) { if (mainImage != null) {
this.mainImage = new MainImageUpdateSpec(); this.mainImage = new MainImageUpdateSpec();

View File

@ -1,7 +1,22 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
public interface RecipeStar { @Entity(name = "RecipeStar")
OffsetDateTime getTimestamp(); @Table(name = "recipe_star")
@Data
public final class RecipeStar {
@EmbeddedId
private RecipeStarId id;
@Column(nullable = false, updatable = false)
private OffsetDateTime timestamp = OffsetDateTime.now();
} }

View File

@ -1,41 +0,0 @@
package app.mealsmadeeasy.api.recipe.star;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
@Entity(name = "RecipeStar")
@Table(name = "recipe_star")
public final class RecipeStarEntity implements RecipeStar {
@EmbeddedId
private RecipeStarId id;
@Column(nullable = false, updatable = false)
private OffsetDateTime timestamp = OffsetDateTime.now();
public RecipeStarId getId() {
return this.id;
}
public void setId(RecipeStarId id) {
this.id = id;
}
public OffsetDateTime getTimestamp() {
return this.timestamp;
}
public void setTimestamp(OffsetDateTime date) {
this.timestamp = date;
}
@Override
public String toString() {
return "RecipeStarEntity(" + this.id + ")";
}
}

View File

@ -7,10 +7,10 @@ import org.springframework.data.jpa.repository.Query;
import java.util.Optional; import java.util.Optional;
public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> { public interface RecipeStarRepository extends JpaRepository<RecipeStar, Long> {
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2") @Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
Optional<RecipeStarEntity> findByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId); Optional<RecipeStar> findByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
@Query("SELECT count(rs) > 0 FROM RecipeStar rs, Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2 AND r.id = rs.id.recipeId AND rs.id.ownerId = ?3") @Query("SELECT count(rs) > 0 FROM RecipeStar rs, Recipe r WHERE r.owner.username = ?1 AND r.slug = ?2 AND r.id = rs.id.recipeId AND rs.id.ownerId = ?3")
boolean isStarer(String ownerUsername, String slug, Integer viewerId); boolean isStarer(String ownerUsername, String slug, Integer viewerId);

View File

@ -22,7 +22,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
@Override @Override
public RecipeStar create(Integer recipeId, Integer ownerId) { public RecipeStar create(Integer recipeId, Integer ownerId) {
final RecipeStarEntity draft = new RecipeStarEntity(); 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.getOwnerId(ownerId);
@ -34,7 +34,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
@Override @Override
public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId( final Optional<RecipeStar> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
recipe.getId(), recipe.getId(),
starer.getId() starer.getId()
); );
@ -47,8 +47,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
@Override @Override
public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer); final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId()) return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId());
.map(RecipeStar.class::cast);
} }
@Override @Override

View File

@ -36,7 +36,7 @@ public class FullRecipeView {
view.setStarCount(starCount); view.setStarCount(starCount);
view.setViewerCount(viewerCount); view.setViewerCount(viewerCount);
view.setMainImage(mainImage); view.setMainImage(mainImage);
view.setIsPublic(recipe.isPublic()); view.setIsPublic(recipe.getIsPublic());
return view; return view;
} }

View File

@ -3,11 +3,13 @@ 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 org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
public final class RecipeInfoView { @Data
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(); final RecipeInfoView view = new RecipeInfoView();
@ -20,7 +22,7 @@ public final class RecipeInfoView {
view.setCookingTime(recipe.getCookingTime()); view.setCookingTime(recipe.getCookingTime());
view.setTotalTime(recipe.getTotalTime()); view.setTotalTime(recipe.getTotalTime());
view.setOwner(UserInfoView.from(recipe.getOwner())); view.setOwner(UserInfoView.from(recipe.getOwner()));
view.setIsPublic(recipe.isPublic()); view.setPublic(recipe.getIsPublic());
view.setStarCount(starCount); view.setStarCount(starCount);
view.setMainImage(mainImage); view.setMainImage(mainImage);
return view; return view;
@ -39,100 +41,4 @@ public final class RecipeInfoView {
private int starCount; private int starCount;
private @Nullable ImageView mainImage; private @Nullable ImageView mainImage;
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 OffsetDateTime getModified() {
return this.modified;
}
public void setModified(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 UserInfoView getOwner() {
return this.owner;
}
public void setOwner(UserInfoView owner) {
this.owner = owner;
}
public boolean getIsPublic() {
return this.isPublic;
}
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
public int getStarCount() {
return this.starCount;
}
public void setStarCount(int starCount) {
this.starCount = starCount;
}
public @Nullable ImageView getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable ImageView mainImage) {
this.mainImage = mainImage;
}
} }

View File

@ -2,7 +2,6 @@ package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher; import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import app.mealsmadeeasy.api.recipe.star.RecipeStar; import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStarId; import app.mealsmadeeasy.api.recipe.star.RecipeStarId;
import java.util.List; import java.util.List;
@ -18,8 +17,8 @@ public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar,
super( super(
List.of(allExpected), List.of(allExpected),
o -> o instanceof RecipeStar, o -> o instanceof RecipeStar,
recipeStar -> ((RecipeStarEntity) recipeStar).getId(), recipeStar -> ((RecipeStar) recipeStar).getId(),
recipeStar -> ((RecipeStarEntity) recipeStar).getId(), recipeStar -> ((RecipeStar) recipeStar).getId(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId()) (id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerId(), id1.getOwnerId()) && Objects.equals(id0.getOwnerId(), id1.getOwnerId())
); );