Change to flyway migrations, many SQL/entity updates.

This commit is contained in:
Jesse Brault 2025-12-26 13:45:27 -06:00
parent 52136a34bd
commit 1fefeaa1da
35 changed files with 320 additions and 196 deletions

View File

@ -49,12 +49,17 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-jackson'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-jackson-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
// Custom
implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
implementation 'io.jsonwebtoken:jjwt-jackson:0.13.0'

View File

@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@ -36,7 +36,7 @@ public class RecipeStarRepositoryTests {
private RecipeEntity getTestRecipe(UserEntity owner) {
final RecipeEntity recipeDraft = new RecipeEntity();
recipeDraft.setCreated(LocalDateTime.now());
recipeDraft.setCreated(OffsetDateTime.now());
recipeDraft.setSlug("test-recipe");
recipeDraft.setOwner(owner);
recipeDraft.setTitle("Test Recipe");
@ -53,7 +53,7 @@ public class RecipeStarRepositoryTests {
final RecipeStarEntity starDraft = new RecipeStarEntity();
final RecipeStarId starId = new RecipeStarId();
starId.setRecipeId(recipe.getId());
starId.setOwnerUsername(owner.getUsername());
starId.getOwnerId(owner.getId());
starDraft.setId(starId);
this.recipeStarRepository.save(starDraft);
@ -61,7 +61,7 @@ public class RecipeStarRepositoryTests {
this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(),
recipe.getSlug(),
owner.getUsername()
owner.getId()
),
is(true)
);
@ -76,7 +76,7 @@ public class RecipeStarRepositoryTests {
this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(),
recipe.getSlug(),
owner.getUsername()
owner.getId()
),
is(false)
);

View File

@ -1,6 +1,5 @@
package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.security.AuthToken;
import org.jetbrains.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -8,6 +7,8 @@ import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/auth")
public final class AuthController {
@ -31,9 +32,9 @@ public final class AuthController {
}
private ResponseEntity<LoginView> getLoginViewResponseEntity(LoginDetails loginDetails) {
final AuthToken refreshToken = loginDetails.getRefreshToken();
final RefreshToken refreshToken = loginDetails.getRefreshToken();
final ResponseCookie refreshCookie = getRefreshTokenCookie(
refreshToken.getToken(),
refreshToken.getToken().toString(),
refreshToken.getLifetime()
);
final var loginView = new LoginView(
@ -60,7 +61,7 @@ public final class AuthController {
@PostMapping("/refresh")
public ResponseEntity<LoginView> refresh(
@CookieValue(value = "refresh-token", required = false) @Nullable String oldRefreshToken
@CookieValue(value = "refresh-token", required = false) @Nullable UUID oldRefreshToken
) throws LoginException {
final LoginDetails loginDetails = this.authService.refresh(oldRefreshToken);
return this.getLoginViewResponseEntity(loginDetails);
@ -68,7 +69,7 @@ public final class AuthController {
@PostMapping("/logout")
public ResponseEntity<?> logout(
@CookieValue(value = "refresh-token", required = false) @Nullable String refreshToken
@CookieValue(value = "refresh-token", required = false) @Nullable UUID refreshToken
) {
if (refreshToken != null) {
this.authService.logout(refreshToken);

View File

@ -2,8 +2,10 @@ package app.mealsmadeeasy.api.auth;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public interface AuthService {
LoginDetails login(String username, String password) throws LoginException;
void logout(String refreshToken);
LoginDetails refresh(@Nullable String refreshToken) throws LoginException;
void logout(UUID refreshToken);
LoginDetails refresh(@Nullable UUID refreshToken) throws LoginException;
}

View File

@ -12,7 +12,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -38,9 +38,9 @@ public class AuthServiceImpl implements AuthService {
private RefreshToken createRefreshToken(UserEntity principal) {
final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity();
refreshTokenDraft.setToken(UUID.randomUUID().toString());
refreshTokenDraft.setIssued(LocalDateTime.now());
refreshTokenDraft.setExpiration(LocalDateTime.now().plusSeconds(this.refreshTokenLifetime));
refreshTokenDraft.setToken(UUID.randomUUID());
refreshTokenDraft.setIssued(OffsetDateTime.now());
refreshTokenDraft.setExpiration(OffsetDateTime.now().plusSeconds(this.refreshTokenLifetime));
refreshTokenDraft.setOwner(principal);
return this.refreshTokenRepository.save(refreshTokenDraft);
}
@ -64,13 +64,13 @@ public class AuthServiceImpl implements AuthService {
@Override
@Transactional
public void logout(String refreshToken) {
public void logout(UUID refreshToken) {
this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete);
}
@Override
@Transactional
public LoginDetails refresh(@Nullable String refreshToken) throws LoginException {
public LoginDetails refresh(@Nullable UUID refreshToken) throws LoginException {
if (refreshToken == null) {
throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided.");
}
@ -83,7 +83,7 @@ public class AuthServiceImpl implements AuthService {
if (old.isRevoked() || old.isDeleted()) {
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token.");
}
if (old.getExpires().isBefore(LocalDateTime.now())) {
if (old.getExpires().isBefore(OffsetDateTime.now())) {
throw new LoginException(LoginExceptionReason.EXPIRED_REFRESH_TOKEN, "Refresh token is expired.");
}

View File

@ -6,9 +6,9 @@ public final class LoginDetails {
private final String username;
private final AuthToken accessToken;
private final AuthToken refreshToken;
private final RefreshToken refreshToken;
public LoginDetails(String username, AuthToken accessToken, AuthToken refreshToken) {
public LoginDetails(String username, AuthToken accessToken, RefreshToken refreshToken) {
this.username = username;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
@ -22,7 +22,7 @@ public final class LoginDetails {
return this.accessToken;
}
public AuthToken getRefreshToken() {
public RefreshToken getRefreshToken() {
return this.refreshToken;
}

View File

@ -1,11 +1,13 @@
package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.security.AuthToken;
import java.time.OffsetDateTime;
import java.util.UUID;
import java.time.LocalDateTime;
public interface RefreshToken extends AuthToken {
LocalDateTime getIssued();
public interface RefreshToken {
UUID getToken();
long getLifetime();
OffsetDateTime getExpires();
OffsetDateTime getIssued();
boolean isRevoked();
boolean isDeleted();
}

View File

@ -3,68 +3,58 @@ package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
@Entity(name = "RefreshToken")
@Table(name = "refresh_token")
public class RefreshTokenEntity implements RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false, updatable = false)
private Long id;
@Column(unique = true, nullable = false)
private String token;
@Column(nullable = false)
private UUID token;
@Column(nullable = false)
private LocalDateTime issued;
private OffsetDateTime issued;
@Column(nullable = false)
private LocalDateTime expiration;
private OffsetDateTime expiration;
@Column(nullable = false)
private Boolean revoked = false;
@JoinColumn(nullable = false)
@ManyToOne
@ManyToOne(optional = false)
@JoinColumn(name = "owner_id", nullable = false)
private UserEntity owner;
@Column(nullable = false)
private Boolean deleted = false;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
@Column(nullable = false)
private Boolean revoked = false;
@Override
public String getToken() {
public UUID getToken() {
return this.token;
}
public void setToken(String token) {
public void setToken(UUID token) {
this.token = token;
}
@Override
public LocalDateTime getIssued() {
public OffsetDateTime getIssued() {
return this.issued;
}
public void setIssued(LocalDateTime issued) {
public void setIssued(OffsetDateTime issued) {
this.issued = issued;
}
@Override
public LocalDateTime getExpires() {
public OffsetDateTime getExpires() {
return this.expiration;
}
public void setExpiration(LocalDateTime expiration) {
public void setExpiration(OffsetDateTime expiration) {
this.expiration = expiration;
}

View File

@ -6,10 +6,11 @@ import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
import java.util.UUID;
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
Optional<RefreshTokenEntity> findByToken(String token);
Optional<RefreshTokenEntity> findByToken(UUID token);
@Modifying
@Transactional

View File

@ -3,13 +3,13 @@ package app.mealsmadeeasy.api.image;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Set;
public interface Image {
Long getId();
LocalDateTime getCreated();
@Nullable LocalDateTime getModified();
Integer getId();
OffsetDateTime getCreated();
@Nullable OffsetDateTime getModified();
String getUserFilename();
String getMimeType();
@Nullable String getAlt();

View File

@ -5,22 +5,23 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity(name = "Image")
@Table(name = "image")
public class S3ImageEntity implements Image {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
private Integer id;
@Column(nullable = false)
private LocalDateTime created = LocalDateTime.now();
private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime modified;
private OffsetDateTime modified;
@Column(nullable = false)
private String userFilename;
@ -47,32 +48,37 @@ public class S3ImageEntity implements Image {
private Boolean isPublic = false;
@ManyToMany
@JoinTable(
name = "image_viewer",
joinColumns = @JoinColumn(name = "image_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>();
@Override
public Long getId() {
public Integer getId() {
return this.id;
}
public void setId(Long id) {
public void setId(Integer id) {
this.id = id;
}
@Override
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public @Nullable LocalDateTime getModified() {
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(LocalDateTime modified) {
public void setModified(OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -18,7 +18,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -225,7 +225,7 @@ public class S3ImageService implements ImageService {
}
}
if (didUpdate) {
entity.setModified(LocalDateTime.now());
entity.setModified(OffsetDateTime.now());
}
return this.imageRepository.save(entity);
}

View File

@ -4,7 +4,7 @@ import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Set;
import java.util.stream.Collectors;
@ -33,8 +33,8 @@ public class ImageView {
}
private String url;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private OffsetDateTime created;
private @Nullable OffsetDateTime modified;
private String filename;
private String mimeType;
private @Nullable String alt;
@ -53,19 +53,19 @@ public class ImageView {
this.url = url;
}
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public @Nullable LocalDateTime getModified() {
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable LocalDateTime modified) {
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -6,13 +6,13 @@ import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Set;
public interface Recipe {
Long getId();
LocalDateTime getCreated();
@Nullable LocalDateTime getModified();
Integer getId();
OffsetDateTime getCreated();
@Nullable OffsetDateTime getModified();
String getSlug();
String getTitle();
@Nullable Integer getPreparationTime();

View File

@ -10,7 +10,7 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
@ -18,14 +18,14 @@ import java.util.Set;
public final class RecipeEntity implements Recipe {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
private Long id;
private Integer id;
@Column(nullable = false)
private LocalDateTime created;
private OffsetDateTime created;
private LocalDateTime modified;
private OffsetDateTime modified;
@Column(nullable = false, unique = true)
private String slug;
@ -57,7 +57,7 @@ public final class RecipeEntity implements Recipe {
private UserEntity owner;
@OneToMany
@JoinColumn(name = "recipeId")
@JoinColumn(name = "recipe_id")
private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe")
@ -67,35 +67,41 @@ public final class RecipeEntity implements Recipe {
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;
@Override
public Long getId() {
public Integer getId() {
return this.id;
}
public void setId(Long id) {
public void setId(Integer id) {
this.id = id;
}
@Override
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public @Nullable LocalDateTime getModified() {
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable LocalDateTime modified) {
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -22,7 +22,7 @@ import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -53,7 +53,7 @@ public class RecipeServiceImpl implements RecipeService {
throw new AccessDeniedException("Must be logged in.");
}
final RecipeEntity draft = new RecipeEntity();
draft.setCreated(LocalDateTime.now());
draft.setCreated(OffsetDateTime.now());
draft.setOwner((UserEntity) owner);
draft.setSlug(spec.getSlug());
draft.setTitle(spec.getTitle());
@ -222,7 +222,7 @@ public class RecipeServiceImpl implements RecipeService {
}
recipe.setMainImage(mainImage);
recipe.setModified(LocalDateTime.now());
recipe.setModified(OffsetDateTime.now());
return this.recipeRepository.save(recipe);
}
@ -279,7 +279,7 @@ public class RecipeServiceImpl implements RecipeService {
if (viewer == null) {
return null;
}
return this.recipeStarRepository.isStarer(username, slug, viewer.getUsername());
return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
}
@Override

View File

@ -4,12 +4,12 @@ import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public interface RecipeComment {
Long getId();
LocalDateTime getCreated();
@Nullable LocalDateTime getModified();
Integer getId();
OffsetDateTime getCreated();
@Nullable OffsetDateTime getModified();
String getRawText();
User getOwner();
Recipe getRecipe();

View File

@ -4,20 +4,21 @@ import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@Entity(name = "RecipeComment")
@Table(name = "recipe_comment")
public final class RecipeCommentEntity implements RecipeComment {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
private Integer id;
@Column(nullable = false, updatable = false)
private LocalDateTime created = LocalDateTime.now();
private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime modified;
private OffsetDateTime modified;
@Lob
@Basic(fetch = FetchType.LAZY)
@ -35,29 +36,29 @@ public final class RecipeCommentEntity implements RecipeComment {
@JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe;
public Long getId() {
public Integer getId() {
return this.id;
}
public void setId(Long id) {
public void setId(Integer id) {
this.id = id;
}
@Override
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
@Override
public LocalDateTime getModified() {
public OffsetDateTime getModified() {
return this.modified;
}
public void setModified(LocalDateTime modified) {
public void setModified(OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -12,7 +12,7 @@ import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import static java.util.Objects.requireNonNull;
@ -43,7 +43,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
) throws RecipeException {
requireNonNull(commenter);
final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setCreated(LocalDateTime.now());
draft.setCreated(OffsetDateTime.now());
draft.setRawText(body.getText());
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
draft.setOwner((UserEntity) commenter);

View File

@ -3,7 +3,7 @@ package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public class RecipeCommentView {
@ -21,35 +21,35 @@ public class RecipeCommentView {
return view;
}
private Long id;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private Integer id;
private OffsetDateTime created;
private @Nullable OffsetDateTime modified;
private String text;
private @Nullable String rawText;
private UserInfoView owner;
private Long recipeId;
private Integer recipeId;
public Long getId() {
public Integer getId() {
return this.id;
}
public void setId(Long id) {
public void setId(Integer id) {
this.id = id;
}
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public @Nullable LocalDateTime getModified() {
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable LocalDateTime modified) {
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}
@ -77,11 +77,11 @@ public class RecipeCommentView {
this.owner = owner;
}
public Long getRecipeId() {
public Integer getRecipeId() {
return this.recipeId;
}
public void setRecipeId(Long recipeId) {
public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId;
}

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe.star;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public interface RecipeStar {
LocalDateTime getDate();
OffsetDateTime getTimestamp();
}

View File

@ -3,17 +3,19 @@ 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.LocalDateTime;
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 LocalDateTime date = LocalDateTime.now();
private OffsetDateTime timestamp = OffsetDateTime.now();
public RecipeStarId getId() {
return this.id;
@ -23,13 +25,12 @@ public final class RecipeStarEntity implements RecipeStar {
this.id = id;
}
@Override
public LocalDateTime getDate() {
return this.date;
public OffsetDateTime getTimestamp() {
return this.timestamp;
}
public void setDate(LocalDateTime date) {
this.date = date;
public void setTimestamp(OffsetDateTime date) {
this.timestamp = date;
}
@Override

View File

@ -8,25 +8,25 @@ import java.util.Objects;
@Embeddable
public class RecipeStarId {
@Column(nullable = false)
private String ownerUsername;
@Column(name = "owner_id", nullable = false)
private Integer ownerId;
@Column(nullable = false)
private Long recipeId;
@Column(name = "recipe_id", nullable = false)
private Integer recipeId;
public String getOwnerUsername() {
return this.ownerUsername;
public Integer getOwnerId() {
return this.ownerId;
}
public void setOwnerUsername(String ownerUsername) {
this.ownerUsername = ownerUsername;
public void getOwnerId(Integer ownerId) {
this.ownerId = ownerId;
}
public Long getRecipeId() {
public Integer getRecipeId() {
return this.recipeId;
}
public void setRecipeId(Long recipeId) {
public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId;
}
@ -34,19 +34,19 @@ public class RecipeStarId {
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof RecipeStarId other) {
return this.recipeId.equals(other.recipeId) && this.ownerUsername.equals(other.ownerUsername);
return this.recipeId.equals(other.recipeId) && this.ownerId.equals(other.ownerId);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(this.recipeId, this.ownerUsername);
return Objects.hash(this.recipeId, this.ownerId);
}
@Override
public String toString() {
return "RecipeStarId(" + this.recipeId + ", " + this.ownerUsername + ")";
return "RecipeStarId(" + this.recipeId + ", " + this.ownerId + ")";
}
}

View File

@ -9,15 +9,15 @@ import java.util.Optional;
public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> {
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2")
Optional<RecipeStarEntity> findByRecipeIdAndOwnerUsername(Long recipeId, String username);
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
Optional<RecipeStarEntity> 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.ownerUsername = ?3")
boolean isStarer(String ownerUsername, String slug, String viewerUsername);
@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);
@Modifying
@Transactional
@Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2")
void deleteByRecipeIdAndOwnerUsername(Long recipeId, String username);
@Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
void deleteByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
}

View File

@ -6,11 +6,11 @@ import app.mealsmadeeasy.api.user.User;
import java.util.Optional;
public interface RecipeStarService {
RecipeStar create(long recipeId, String ownerUsername);
RecipeStar create(Integer recipeId, Integer ownerId);
RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
void delete(long recipeId, String ownerUsername);
void delete(Integer recipeId, Integer ownerId);
void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
}

View File

@ -6,7 +6,7 @@ import app.mealsmadeeasy.api.recipe.RecipeService;
import app.mealsmadeeasy.api.user.User;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Optional;
@Service
@ -21,45 +21,45 @@ public class RecipeStarServiceImpl implements RecipeStarService {
}
@Override
public RecipeStar create(long recipeId, String ownerUsername) {
public RecipeStar create(Integer recipeId, Integer ownerId) {
final RecipeStarEntity draft = new RecipeStarEntity();
final RecipeStarId id = new RecipeStarId();
id.setRecipeId(recipeId);
id.setOwnerUsername(ownerUsername);
id.getOwnerId(ownerId);
draft.setId(id);
draft.setDate(LocalDateTime.now());
draft.setTimestamp(OffsetDateTime.now());
return this.recipeStarRepository.save(draft);
}
@Override
public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerUsername(
final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
recipe.getId(),
starer.getUsername()
starer.getId()
);
if (existing.isPresent()) {
return existing.get();
}
return this.create(recipe.getId(), starer.getUsername());
return this.create(recipe.getId(), starer.getId());
}
@Override
public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
return this.recipeStarRepository.findByRecipeIdAndOwnerUsername(recipe.getId(), starer.getUsername())
return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId())
.map(RecipeStar.class::cast);
}
@Override
public void delete(long recipeId, String ownerUsername) {
this.recipeStarRepository.deleteByRecipeIdAndOwnerUsername(recipeId, ownerUsername);
public void delete(Integer recipeId, Integer ownerId) {
this.recipeStarRepository.deleteByRecipeIdAndOwnerId(recipeId, ownerId);
}
@Override
public void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
this.delete(recipe.getId(), starer.getUsername());
this.delete(recipe.getId(), starer.getId());
}
}

View File

@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public class FullRecipeView {
@ -41,8 +41,8 @@ public class FullRecipeView {
}
private long id;
private LocalDateTime created;
private @Nullable LocalDateTime modified;
private OffsetDateTime created;
private @Nullable OffsetDateTime modified;
private String slug;
private String title;
private @Nullable Integer preparationTime;
@ -64,19 +64,19 @@ public class FullRecipeView {
this.id = id;
}
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public @Nullable LocalDateTime getModified() {
public @Nullable OffsetDateTime getModified() {
return this.modified;
}
public void setModified(@Nullable LocalDateTime modified) {
public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -5,7 +5,7 @@ import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public final class RecipeInfoView {
@ -27,8 +27,8 @@ public final class RecipeInfoView {
}
private long id;
private LocalDateTime created;
private LocalDateTime modified;
private OffsetDateTime created;
private OffsetDateTime modified;
private String slug;
private String title;
private @Nullable Integer preparationTime;
@ -47,19 +47,19 @@ public final class RecipeInfoView {
this.id = id;
}
public LocalDateTime getCreated() {
public OffsetDateTime getCreated() {
return this.created;
}
public void setCreated(LocalDateTime created) {
public void setCreated(OffsetDateTime created) {
this.created = created;
}
public LocalDateTime getModified() {
public OffsetDateTime getModified() {
return this.modified;
}
public void setModified(LocalDateTime modified) {
public void setModified(OffsetDateTime modified) {
this.modified = modified;
}

View File

@ -6,7 +6,7 @@ import java.util.Set;
public interface User extends UserDetails {
Long getId();
Integer getId();
String getEmail();
void setEmail(String email);

View File

@ -24,7 +24,7 @@ public final class UserEntity implements User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
private Integer id;
@Column(unique = true, nullable = false)
private String username;
@ -51,11 +51,11 @@ public final class UserEntity implements User {
private Boolean credentialsExpired;
@Override
public Long getId() {
public Integer getId() {
return this.id;
}
public void setId(Long id) {
public void setId(Integer id) {
this.id = id;
}

View File

@ -2,18 +2,19 @@ package app.mealsmadeeasy.api.user;
import jakarta.persistence.*;
@Entity
@Entity(name = "UserGrantedAuthority")
@Table(name = "user_granted_authority")
public final class UserGrantedAuthorityEntity implements UserGrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
private Integer id;
private String authority;
@ManyToOne
@JoinColumn(name = "user_entity_id")
@JoinColumn(name = "user_id")
private UserEntity userEntity;
@Override

View File

@ -11,14 +11,14 @@ public class UserInfoView {
return userInfoView;
}
private long id;
private Integer id;
private String username;
public long getId() {
public Integer getId() {
return this.id;
}
public void setId(long id) {
public void setId(Integer id) {
this.id = id;
}

View File

@ -1,5 +1,5 @@
spring.application.name=meals-made-easy-api
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:meals_made_easy_api}
spring.datasource.username=${POSTGRES_USER:meals-made-easy-api-user}
spring.datasource.password=${POSTGRES_PASSWORD}

View File

@ -0,0 +1,108 @@
CREATE TABLE "user" (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
locked BOOLEAN NOT NULL,
expired BOOLEAN NOT NULL,
enabled BOOLEAN NOT NULL,
credentials_expired BOOLEAN NOT NULL
);
CREATE TABLE user_granted_authority (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id INT NOT NULL,
authority VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES "user"
);
CREATE TABLE refresh_token (
token UUID PRIMARY KEY,
issued TIMESTAMPTZ(6) NOT NULL,
expiration TIMESTAMPTZ(6) NOT NULL,
owner_id INT NOT NULL,
deleted BOOLEAN NOT NULL,
revoked BOOLEAN NOT NULL,
FOREIGN KEY (owner_id) REFERENCES "user"
);
CREATE TABLE image (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
is_public BOOLEAN NOT NULL,
object_name VARCHAR(255) NOT NULL,
user_filename VARCHAR(255) NOT NULL,
mime_type VARCHAR(255) NOT NULL,
alt VARCHAR(255),
caption VARCHAR(255),
height INTEGER,
width INTEGER,
FOREIGN KEY (owner_id) REFERENCES "user"
);
CREATE TABLE image_viewer (
image_id INT NOT NULL,
viewer_id INT NOT NULL,
PRIMARY KEY (image_id, viewer_id),
FOREIGN KEY (image_id) REFERENCES image,
FOREIGN KEY (viewer_id) REFERENCES "user"
);
CREATE TABLE recipe (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
slug VARCHAR(255) NOT NULL,
is_public BOOLEAN NOT NULL,
title VARCHAR(255) NOT NULL,
raw_text TEXT NOT NULL,
cached_rendered_text TEXT,
main_image_id INT,
preparation_time INT,
cooking_time INT,
total_time INT,
FOREIGN KEY (owner_id) REFERENCES "user",
FOREIGN KEY (main_image_id) REFERENCES image
);
CREATE INDEX recipe_username_slug ON recipe (owner_id, slug);
CREATE TABLE recipe_viewer (
recipe_id INT NOT NULL,
viewer_id INT NOT NULL,
PRIMARY KEY (recipe_id, viewer_id),
FOREIGN KEY (recipe_id) REFERENCES recipe,
FOREIGN KEY (viewer_id) REFERENCES "user"
);
CREATE TABLE recipe_comment (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created TIMESTAMPTZ(6) NOT NULL,
modified TIMESTAMPTZ(6),
owner_id INT NOT NULL,
recipe_id INT NOT NULL,
raw_text TEXT NOT NULL,
cached_rendered_text TEXT,
FOREIGN KEY (owner_id) REFERENCES "user",
FOREIGN KEY (recipe_id) REFERENCES recipe
);
CREATE TABLE recipe_star (
timestamp TIMESTAMPTZ(6) NOT NULL,
recipe_id INT NOT NULL,
owner_id INT NOT NULL,
PRIMARY KEY (recipe_id, owner_id),
FOREIGN KEY (recipe_id) references recipe,
FOREIGN KEY (owner_id) references "user"
);

View File

@ -21,7 +21,7 @@ public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar,
recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerUsername(), id1.getOwnerUsername())
&& Objects.equals(id0.getOwnerId(), id1.getOwnerId())
);
}