MME-8 Add endpoints necessary for recipe main image selection. Refactor SliceView.
This commit is contained in:
parent
02c7e8887e
commit
f9c1d41501
@ -130,6 +130,24 @@ public class ImageControllerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getOwnedImages() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
this.seedImage(owner);
|
||||
this.mockMvc.perform(
|
||||
get("/images")
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.count").value(1))
|
||||
.andExpect(jsonPath("$.content").isArray())
|
||||
.andExpect(jsonPath("$.content").isNotEmpty())
|
||||
.andExpect(jsonPath("$.content[0].url").isNotEmpty())
|
||||
.andExpect(jsonPath("$.slice.size").isNotEmpty())
|
||||
.andExpect(jsonPath("$.slice.number", is(0)))
|
||||
.andExpect(jsonPath("$.slice.hasNext", is(false)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getImageForOwner() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
@ -204,10 +222,6 @@ public class ImageControllerTests {
|
||||
.param("caption", "HAL 9000, from 2001: A Space Odyssey")
|
||||
.param("isPublic", "true")
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
.with(req -> {
|
||||
req.setMethod("PUT");
|
||||
return req;
|
||||
})
|
||||
)
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.created").exists())
|
||||
@ -231,7 +245,7 @@ public class ImageControllerTests {
|
||||
final ImageUpdateBody body = new ImageUpdateBody();
|
||||
body.setAlt("HAL 9000");
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(owner, image))
|
||||
put(getImageUrl(owner, image))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -249,7 +263,7 @@ public class ImageControllerTests {
|
||||
final ImageUpdateBody body = new ImageUpdateBody();
|
||||
body.setCaption("HAL 9000 from 2001: A Space Odyssey");
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(owner, image))
|
||||
put(getImageUrl(owner, image))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -267,7 +281,7 @@ public class ImageControllerTests {
|
||||
final ImageUpdateBody body = new ImageUpdateBody();
|
||||
body.setIsPublic(true);
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(owner, image))
|
||||
put(getImageUrl(owner, image))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -289,7 +303,7 @@ public class ImageControllerTests {
|
||||
body.setViewersToAdd(viewerUsernames);
|
||||
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(owner, image))
|
||||
put(getImageUrl(owner, image))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -322,7 +336,7 @@ public class ImageControllerTests {
|
||||
body.setViewersToRemove(Set.of(ownerViewerImage.viewer().getUsername()));
|
||||
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
put(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -339,7 +353,7 @@ public class ImageControllerTests {
|
||||
final ImageUpdateBody body = new ImageUpdateBody();
|
||||
body.setClearAllViewers(true);
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
put(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -355,7 +369,7 @@ public class ImageControllerTests {
|
||||
final String accessToken = this.getAccessToken(ownerViewerImage.viewer()); // viewer
|
||||
final ImageUpdateBody body = new ImageUpdateBody();
|
||||
this.mockMvc.perform(
|
||||
post(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
put(getImageUrl(ownerViewerImage.owner(), ownerViewerImage.image()))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(this.objectMapper.writeValueAsString(body))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
@ -389,4 +403,42 @@ public class ImageControllerTests {
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenImageExists_existsIsTrue() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final Image image = this.seedImage(owner);
|
||||
|
||||
this.mockMvc.perform(
|
||||
get("/images/{username}/{filename}/exists", image.getOwner().getUsername(), image.getUserFilename())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.exists").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenImageDoesNotExist_existsIsFalse() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
this.mockMvc.perform(
|
||||
get("/images/{username}/fake-filename.jpeg/exists", owner.getUsername())
|
||||
.header("Authorization", "Bearer " + this.getAccessToken(owner))
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.exists").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cannotDoExistsOnInaccessibleImage() throws Exception {
|
||||
final User owner = this.seedUser();
|
||||
final User viewer = this.seedUser();
|
||||
final Image image = this.seedImage(owner);
|
||||
final String viewerAccessToken = this.getAccessToken(viewer);
|
||||
|
||||
this.mockMvc.perform(
|
||||
get("/images/{username}/{filename}/exists", owner.getUsername(), image.getUserFilename())
|
||||
.header("Authorization", "Bearer " + viewerAccessToken)
|
||||
)
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -182,6 +182,7 @@ public class RecipesControllerTests {
|
||||
final Recipe recipe = this.createTestRecipe(owner, true);
|
||||
this.mockMvc.perform(get("/recipes"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.count").isNotEmpty())
|
||||
.andExpect(jsonPath("$.slice.number").value(0))
|
||||
.andExpect(jsonPath("$.slice.size").value(20))
|
||||
.andExpect(jsonPath("$.content").isArray())
|
||||
@ -200,6 +201,7 @@ public class RecipesControllerTests {
|
||||
.header("Authorization", "Bearer " + loginDetails.getAccessToken().getToken())
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.count").isNotEmpty())
|
||||
.andExpect(jsonPath("$.slice.number").value(0))
|
||||
.andExpect(jsonPath("$.slice.size").value(20))
|
||||
.andExpect(jsonPath("$.content").isArray())
|
||||
|
||||
@ -5,12 +5,16 @@ import app.mealsmadeeasy.api.image.converter.ImageToViewConverter;
|
||||
import app.mealsmadeeasy.api.image.converter.ImageUpdateBodyToSpecConverter;
|
||||
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||
import app.mealsmadeeasy.api.sliceview.SliceView;
|
||||
import app.mealsmadeeasy.api.sliceview.SliceViewService;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import app.mealsmadeeasy.api.user.UserService;
|
||||
import app.mealsmadeeasy.api.util.AccessDeniedView;
|
||||
import app.mealsmadeeasy.api.util.ResourceExistsView;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -34,6 +38,7 @@ public class ImageController {
|
||||
private final UserService userService;
|
||||
private final ImageToViewConverter imageToViewConverter;
|
||||
private final ImageUpdateBodyToSpecConverter imageUpdateBodyToSpecConverter;
|
||||
private final SliceViewService sliceViewService;
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<AccessDeniedView> onAccessDenied(AccessDeniedException e) {
|
||||
@ -48,6 +53,19 @@ public class ImageController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<SliceView<ImageView>> getOwnedImages(
|
||||
@AuthenticationPrincipal User principal,
|
||||
Pageable pageable
|
||||
) {
|
||||
final Slice<Image> images = this.imageService.getOwnedImages(principal, pageable);
|
||||
final Slice<ImageView> imageViews = images.map(image ->
|
||||
this.imageToViewConverter.convert(image, principal, false)
|
||||
);
|
||||
final int count = this.imageService.countOwnedImages(principal);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(imageViews, count));
|
||||
}
|
||||
|
||||
@GetMapping("/{username}/{filename}")
|
||||
public ResponseEntity<InputStreamResource> getImage(
|
||||
@AuthenticationPrincipal User principal,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package app.mealsmadeeasy.api.image;
|
||||
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
@ -20,4 +22,9 @@ public interface ImageRepository extends JpaRepository<Image, Integer> {
|
||||
@Query("SELECT image from Image image WHERE image.owner.username = ?1 AND image.userFilename = ?2")
|
||||
Optional<Image> findByOwnerUsernameAndFilename(String username, String filename);
|
||||
|
||||
@Query("SELECT i FROM Image i WHERE i.owner = ?1")
|
||||
Slice<Image> findAllOwnedBy(User user, Pageable pageable);
|
||||
|
||||
int countAllByOwner(User owner);
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
||||
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@ -20,6 +22,8 @@ public interface ImageService {
|
||||
|
||||
InputStream getImageContent(Image image, @Nullable User viewer) throws IOException;
|
||||
List<Image> getImagesOwnedBy(User user);
|
||||
Slice<Image> getOwnedImages(User user, Pageable pageable);
|
||||
int countOwnedImages(User user);
|
||||
|
||||
boolean exists(User viewer, String username, String filename);
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndFilenameException;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.security.access.prepost.PostAuthorize;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -196,6 +198,16 @@ public class S3ImageService implements ImageService {
|
||||
return new ArrayList<>(this.imageRepository.findAllByOwner(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Slice<Image> getOwnedImages(User user, Pageable pageable) {
|
||||
return this.imageRepository.findAllOwnedBy(user, pageable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countOwnedImages(User user) {
|
||||
return this.imageRepository.countAllByOwner(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
@PreAuthorize("@imageSecurity.canSeeExists(#viewer, #username, #filename)")
|
||||
public boolean exists(User viewer, String username, String filename) {
|
||||
|
||||
@ -69,4 +69,9 @@ public interface RecipeRepository extends JpaRepository<Recipe, Integer> {
|
||||
)
|
||||
List<Recipe> searchByEmbeddingAndIsPublic(float[] queryEmbedding, float similarity);
|
||||
|
||||
int countByIsPublicIsTrue();
|
||||
|
||||
@Query("SELECT count(r) FROM Recipe r WHERE ?1 MEMBER OF r.viewers OR r.isPublic IS TRUE")
|
||||
int countViewableBy(User viewer);
|
||||
|
||||
}
|
||||
|
||||
@ -114,6 +114,10 @@ public class RecipeService {
|
||||
return this.recipeRepository.findAllViewableBy(viewer, pageable);
|
||||
}
|
||||
|
||||
public int countViewableBy(User viewer) {
|
||||
return this.recipeRepository.countViewableBy(viewer);
|
||||
}
|
||||
|
||||
public List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer) {
|
||||
return List.copyOf(
|
||||
this.recipeRepository.findAllViewableByStarsGreaterThanEqual(minimumStars, viewer)
|
||||
@ -124,6 +128,10 @@ public class RecipeService {
|
||||
return this.recipeRepository.findAllByIsPublicIsTrue(pageable);
|
||||
}
|
||||
|
||||
public int countPublicRecipes() {
|
||||
return this.recipeRepository.countByIsPublicIsTrue();
|
||||
}
|
||||
|
||||
public List<Recipe> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
|
||||
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
|
||||
final List<Recipe> results;
|
||||
|
||||
@ -14,6 +14,7 @@ import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
||||
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
||||
import app.mealsmadeeasy.api.sliceview.SliceView;
|
||||
import app.mealsmadeeasy.api.sliceview.SliceViewService;
|
||||
import app.mealsmadeeasy.api.user.User;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -78,7 +79,7 @@ public class RecipesController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getRecipeInfoViews(
|
||||
public ResponseEntity<SliceView<RecipeInfoView>> getRecipeInfoViews(
|
||||
Pageable pageable,
|
||||
@AuthenticationPrincipal User user
|
||||
) {
|
||||
@ -87,13 +88,15 @@ public class RecipesController {
|
||||
final Slice<RecipeInfoView> publicRecipeInfoViews = publicRecipes.map(
|
||||
recipe -> this.recipeToInfoViewConverter.convert(recipe, null)
|
||||
);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(publicRecipeInfoViews));
|
||||
final int count = this.recipeService.countPublicRecipes();
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(publicRecipeInfoViews, count));
|
||||
} else {
|
||||
final Slice<Recipe> recipes = this.recipeService.getViewableBy(pageable, user);
|
||||
final Slice<RecipeInfoView> recipeInfoViews = recipes.map(
|
||||
recipe -> this.recipeToInfoViewConverter.convert(recipe, user)
|
||||
);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(recipeInfoViews));
|
||||
final int count = this.recipeService.countViewableBy(user);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(recipeInfoViews, count));
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,7 +154,7 @@ public class RecipesController {
|
||||
}
|
||||
|
||||
@GetMapping("/{username}/{slug}/comments")
|
||||
public ResponseEntity<Map<String, Object>> getComments(
|
||||
public ResponseEntity<SliceView<RecipeCommentView>> getComments(
|
||||
@PathVariable String username,
|
||||
@PathVariable String slug,
|
||||
Pageable pageable,
|
||||
@ -163,7 +166,8 @@ public class RecipesController {
|
||||
pageable,
|
||||
principal
|
||||
);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(slice));
|
||||
final int count = this.recipeCommentService.countComments(username, slug);
|
||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(slice, count));
|
||||
}
|
||||
|
||||
@PostMapping("/{username}/{slug}/comments")
|
||||
|
||||
@ -4,8 +4,12 @@ import app.mealsmadeeasy.api.recipe.Recipe;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface RecipeCommentRepository extends JpaRepository<RecipeComment, Integer> {
|
||||
void deleteAllByRecipe(Recipe recipe);
|
||||
Slice<RecipeComment> findAllByRecipe(Recipe recipe, Pageable pageable);
|
||||
|
||||
@Query("SELECT count(rc) FROM RecipeComment rc WHERE rc.recipe.owner.username = ?1 AND rc.recipe.slug = ?2")
|
||||
int countByUsernameAndSlug(String username, String slug);
|
||||
}
|
||||
|
||||
@ -10,4 +10,5 @@ public interface RecipeCommentService {
|
||||
Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer);
|
||||
RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) ;
|
||||
void delete(Integer commentId, User modifier);
|
||||
int countComments(String recipeUsername, String recipeSlug);
|
||||
}
|
||||
|
||||
@ -105,4 +105,9 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
|
||||
this.recipeCommentRepository.delete(entityToDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countComments(String recipeUsername, String recipeSlug) {
|
||||
return this.recipeCommentRepository.countByUsernameAndSlug(recipeUsername, recipeSlug);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package app.mealsmadeeasy.api.sliceview;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SliceView<T>(List<T> content, SliceViewMeta slice, Integer count) {
|
||||
|
||||
public record SliceViewMeta(Integer size, Integer number, Boolean hasNext) {}
|
||||
|
||||
}
|
||||
@ -3,21 +3,16 @@ package app.mealsmadeeasy.api.sliceview;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SliceViewService {
|
||||
|
||||
public Map<String, Object> getSliceView(Slice<?> slice) {
|
||||
final Map<String, Object> view = new HashMap<>();
|
||||
view.put("content", slice.getContent());
|
||||
final Map<String, Object> sliceInfo = new HashMap<>();
|
||||
sliceInfo.put("size", slice.getSize());
|
||||
sliceInfo.put("number", slice.getNumber());
|
||||
sliceInfo.put("hasNext", slice.hasNext());
|
||||
view.put("slice", sliceInfo);
|
||||
return view;
|
||||
public <T> SliceView<T> getSliceView(Slice<T> slice, int count) {
|
||||
final SliceView.SliceViewMeta meta = new SliceView.SliceViewMeta(
|
||||
slice.getSize(),
|
||||
slice.getNumber(),
|
||||
slice.hasNext()
|
||||
);
|
||||
return new SliceView<>(slice.getContent(), meta, count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user