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-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc' implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-jackson' implementation 'org.springframework.boot:spring-boot-starter-jackson'
implementation 'org.springframework.boot:spring-boot-starter-flyway'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-jackson-test' testImplementation 'org.springframework.boot:spring-boot-starter-jackson-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'
// flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
// Custom // Custom
implementation 'io.jsonwebtoken:jjwt-api:0.13.0' implementation 'io.jsonwebtoken:jjwt-api:0.13.0'
implementation 'io.jsonwebtoken:jjwt-jackson: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.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
@ -36,7 +36,7 @@ public class RecipeStarRepositoryTests {
private RecipeEntity getTestRecipe(UserEntity owner) { private RecipeEntity getTestRecipe(UserEntity owner) {
final RecipeEntity recipeDraft = new RecipeEntity(); final RecipeEntity recipeDraft = new RecipeEntity();
recipeDraft.setCreated(LocalDateTime.now()); recipeDraft.setCreated(OffsetDateTime.now());
recipeDraft.setSlug("test-recipe"); recipeDraft.setSlug("test-recipe");
recipeDraft.setOwner(owner); recipeDraft.setOwner(owner);
recipeDraft.setTitle("Test Recipe"); recipeDraft.setTitle("Test Recipe");
@ -53,7 +53,7 @@ public class RecipeStarRepositoryTests {
final RecipeStarEntity starDraft = new RecipeStarEntity(); final RecipeStarEntity starDraft = new RecipeStarEntity();
final RecipeStarId starId = new RecipeStarId(); final RecipeStarId starId = new RecipeStarId();
starId.setRecipeId(recipe.getId()); starId.setRecipeId(recipe.getId());
starId.setOwnerUsername(owner.getUsername()); starId.getOwnerId(owner.getId());
starDraft.setId(starId); starDraft.setId(starId);
this.recipeStarRepository.save(starDraft); this.recipeStarRepository.save(starDraft);
@ -61,7 +61,7 @@ public class RecipeStarRepositoryTests {
this.recipeStarRepository.isStarer( this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
owner.getUsername() owner.getId()
), ),
is(true) is(true)
); );
@ -76,7 +76,7 @@ public class RecipeStarRepositoryTests {
this.recipeStarRepository.isStarer( this.recipeStarRepository.isStarer(
recipe.getOwner().getUsername(), recipe.getOwner().getUsername(),
recipe.getSlug(), recipe.getSlug(),
owner.getUsername() owner.getId()
), ),
is(false) is(false)
); );

View File

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

View File

@ -2,8 +2,10 @@ package app.mealsmadeeasy.api.auth;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public interface AuthService { public interface AuthService {
LoginDetails login(String username, String password) throws LoginException; LoginDetails login(String username, String password) throws LoginException;
void logout(String refreshToken); void logout(UUID refreshToken);
LoginDetails refresh(@Nullable String refreshToken) throws LoginException; 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.security.core.AuthenticationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -38,9 +38,9 @@ public class AuthServiceImpl implements AuthService {
private RefreshToken createRefreshToken(UserEntity principal) { private RefreshToken createRefreshToken(UserEntity principal) {
final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity(); final RefreshTokenEntity refreshTokenDraft = new RefreshTokenEntity();
refreshTokenDraft.setToken(UUID.randomUUID().toString()); refreshTokenDraft.setToken(UUID.randomUUID());
refreshTokenDraft.setIssued(LocalDateTime.now()); refreshTokenDraft.setIssued(OffsetDateTime.now());
refreshTokenDraft.setExpiration(LocalDateTime.now().plusSeconds(this.refreshTokenLifetime)); refreshTokenDraft.setExpiration(OffsetDateTime.now().plusSeconds(this.refreshTokenLifetime));
refreshTokenDraft.setOwner(principal); refreshTokenDraft.setOwner(principal);
return this.refreshTokenRepository.save(refreshTokenDraft); return this.refreshTokenRepository.save(refreshTokenDraft);
} }
@ -64,13 +64,13 @@ public class AuthServiceImpl implements AuthService {
@Override @Override
@Transactional @Transactional
public void logout(String refreshToken) { public void logout(UUID refreshToken) {
this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete); this.refreshTokenRepository.findByToken(refreshToken).ifPresent(this.refreshTokenRepository::delete);
} }
@Override @Override
@Transactional @Transactional
public LoginDetails refresh(@Nullable String refreshToken) throws LoginException { public LoginDetails refresh(@Nullable UUID refreshToken) throws LoginException {
if (refreshToken == null) { if (refreshToken == null) {
throw new LoginException(LoginExceptionReason.NO_REFRESH_TOKEN, "No refresh token provided."); 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()) { if (old.isRevoked() || old.isDeleted()) {
throw new LoginException(LoginExceptionReason.INVALID_REFRESH_TOKEN, "Invalid refresh token."); 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."); 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 String username;
private final AuthToken accessToken; 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.username = username;
this.accessToken = accessToken; this.accessToken = accessToken;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
@ -22,7 +22,7 @@ public final class LoginDetails {
return this.accessToken; return this.accessToken;
} }
public AuthToken getRefreshToken() { public RefreshToken getRefreshToken() {
return this.refreshToken; return this.refreshToken;
} }

View File

@ -1,11 +1,13 @@
package app.mealsmadeeasy.api.auth; 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 {
UUID getToken();
public interface RefreshToken extends AuthToken { long getLifetime();
LocalDateTime getIssued(); OffsetDateTime getExpires();
OffsetDateTime getIssued();
boolean isRevoked(); boolean isRevoked();
boolean isDeleted(); boolean isDeleted();
} }

View File

@ -3,68 +3,58 @@ package app.mealsmadeeasy.api.auth;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.UUID;
@Entity(name = "RefreshToken") @Entity(name = "RefreshToken")
@Table(name = "refresh_token")
public class RefreshTokenEntity implements RefreshToken { public class RefreshTokenEntity implements RefreshToken {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @Column(nullable = false)
@Column(nullable = false, updatable = false) private UUID token;
private Long id;
@Column(unique = true, nullable = false)
private String token;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime issued; private OffsetDateTime issued;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime expiration; private OffsetDateTime expiration;
@Column(nullable = false) @ManyToOne(optional = false)
private Boolean revoked = false; @JoinColumn(name = "owner_id", nullable = false)
@JoinColumn(nullable = false)
@ManyToOne
private UserEntity owner; private UserEntity owner;
@Column(nullable = false) @Column(nullable = false)
private Boolean deleted = false; private Boolean deleted = false;
public Long getId() { @Column(nullable = false)
return this.id; private Boolean revoked = false;
}
public void setId(Long id) {
this.id = id;
}
@Override @Override
public String getToken() { public UUID getToken() {
return this.token; return this.token;
} }
public void setToken(String token) { public void setToken(UUID token) {
this.token = token; this.token = token;
} }
@Override @Override
public LocalDateTime getIssued() { public OffsetDateTime getIssued() {
return this.issued; return this.issued;
} }
public void setIssued(LocalDateTime issued) { public void setIssued(OffsetDateTime issued) {
this.issued = issued; this.issued = issued;
} }
@Override @Override
public LocalDateTime getExpires() { public OffsetDateTime getExpires() {
return this.expiration; return this.expiration;
} }
public void setExpiration(LocalDateTime expiration) { public void setExpiration(OffsetDateTime expiration) {
this.expiration = 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 org.springframework.data.jpa.repository.Query;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> { public interface RefreshTokenRepository extends JpaRepository<RefreshTokenEntity, Long> {
Optional<RefreshTokenEntity> findByToken(String token); Optional<RefreshTokenEntity> findByToken(UUID token);
@Modifying @Modifying
@Transactional @Transactional

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -225,7 +225,7 @@ public class S3ImageService implements ImageService {
} }
} }
if (didUpdate) { if (didUpdate) {
entity.setModified(LocalDateTime.now()); entity.setModified(OffsetDateTime.now());
} }
return this.imageRepository.save(entity); 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 app.mealsmadeeasy.api.user.view.UserInfoView;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -33,8 +33,8 @@ public class ImageView {
} }
private String url; private String url;
private LocalDateTime created; private OffsetDateTime created;
private @Nullable LocalDateTime modified; private @Nullable OffsetDateTime modified;
private String filename; private String filename;
private String mimeType; private String mimeType;
private @Nullable String alt; private @Nullable String alt;
@ -53,19 +53,19 @@ public class ImageView {
this.url = url; this.url = url;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

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

View File

@ -10,7 +10,7 @@ import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -18,14 +18,14 @@ import java.util.Set;
public final class RecipeEntity implements Recipe { public final class RecipeEntity implements Recipe {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private Long id; private Integer id;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime created; private OffsetDateTime created;
private LocalDateTime modified; private OffsetDateTime modified;
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String slug; private String slug;
@ -57,7 +57,7 @@ public final class RecipeEntity implements Recipe {
private UserEntity owner; private UserEntity owner;
@OneToMany @OneToMany
@JoinColumn(name = "recipeId") @JoinColumn(name = "recipe_id")
private Set<RecipeStarEntity> stars = new HashSet<>(); private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe") @OneToMany(mappedBy = "recipe")
@ -67,35 +67,41 @@ public final class RecipeEntity implements Recipe {
private Boolean isPublic = false; private Boolean isPublic = false;
@ManyToMany @ManyToMany
@JoinTable(
name = "recipe_viewer",
joinColumns = @JoinColumn(name = "recipe_id"),
inverseJoinColumns = @JoinColumn(name = "viewer_id")
)
private Set<UserEntity> viewers = new HashSet<>(); private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne @ManyToOne
@JoinColumn(name = "main_image_id")
private S3ImageEntity mainImage; private S3ImageEntity mainImage;
@Override @Override
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
@Override @Override
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
@Override @Override
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = 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.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -53,7 +53,7 @@ public class RecipeServiceImpl implements RecipeService {
throw new AccessDeniedException("Must be logged in."); throw new AccessDeniedException("Must be logged in.");
} }
final RecipeEntity draft = new RecipeEntity(); final RecipeEntity draft = new RecipeEntity();
draft.setCreated(LocalDateTime.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());
@ -222,7 +222,7 @@ public class RecipeServiceImpl implements RecipeService {
} }
recipe.setMainImage(mainImage); recipe.setMainImage(mainImage);
recipe.setModified(LocalDateTime.now()); recipe.setModified(OffsetDateTime.now());
return this.recipeRepository.save(recipe); return this.recipeRepository.save(recipe);
} }
@ -279,7 +279,7 @@ public class RecipeServiceImpl implements RecipeService {
if (viewer == null) { if (viewer == null) {
return null; return null;
} }
return this.recipeStarRepository.isStarer(username, slug, viewer.getUsername()); return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
} }
@Override @Override

View File

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

View File

@ -4,20 +4,21 @@ import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity; import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Entity(name = "RecipeComment") @Entity(name = "RecipeComment")
@Table(name = "recipe_comment")
public final class RecipeCommentEntity implements RecipeComment { public final class RecipeCommentEntity implements RecipeComment {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false) @Column(nullable = false)
private Long id; private Integer id;
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private LocalDateTime created = LocalDateTime.now(); private OffsetDateTime created = OffsetDateTime.now();
private LocalDateTime modified; private OffsetDateTime modified;
@Lob @Lob
@Basic(fetch = FetchType.LAZY) @Basic(fetch = FetchType.LAZY)
@ -35,29 +36,29 @@ public final class RecipeCommentEntity implements RecipeComment {
@JoinColumn(name = "recipe_id", nullable = false, updatable = false) @JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe; private RecipeEntity recipe;
public Long getId() { public Integer getId() {
return this.id; return this.id;
} }
public void setId(Long id) { public void setId(Integer id) {
this.id = id; this.id = id;
} }
@Override @Override
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
@Override @Override
public LocalDateTime getModified() { public OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(LocalDateTime modified) { public void setModified(OffsetDateTime modified) {
this.modified = 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.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@ -43,7 +43,7 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
) throws RecipeException { ) throws RecipeException {
requireNonNull(commenter); requireNonNull(commenter);
final RecipeCommentEntity draft = new RecipeCommentEntity(); final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setCreated(LocalDateTime.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);

View File

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

View File

@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe.star; package app.mealsmadeeasy.api.recipe.star;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public interface RecipeStar { 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.Column;
import jakarta.persistence.EmbeddedId; import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Entity(name = "RecipeStar") @Entity(name = "RecipeStar")
@Table(name = "recipe_star")
public final class RecipeStarEntity implements RecipeStar { public final class RecipeStarEntity implements RecipeStar {
@EmbeddedId @EmbeddedId
private RecipeStarId id; private RecipeStarId id;
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private LocalDateTime date = LocalDateTime.now(); private OffsetDateTime timestamp = OffsetDateTime.now();
public RecipeStarId getId() { public RecipeStarId getId() {
return this.id; return this.id;
@ -23,13 +25,12 @@ public final class RecipeStarEntity implements RecipeStar {
this.id = id; this.id = id;
} }
@Override public OffsetDateTime getTimestamp() {
public LocalDateTime getDate() { return this.timestamp;
return this.date;
} }
public void setDate(LocalDateTime date) { public void setTimestamp(OffsetDateTime date) {
this.date = date; this.timestamp = date;
} }
@Override @Override

View File

@ -8,25 +8,25 @@ import java.util.Objects;
@Embeddable @Embeddable
public class RecipeStarId { public class RecipeStarId {
@Column(nullable = false) @Column(name = "owner_id", nullable = false)
private String ownerUsername; private Integer ownerId;
@Column(nullable = false) @Column(name = "recipe_id", nullable = false)
private Long recipeId; private Integer recipeId;
public String getOwnerUsername() { public Integer getOwnerId() {
return this.ownerUsername; return this.ownerId;
} }
public void setOwnerUsername(String ownerUsername) { public void getOwnerId(Integer ownerId) {
this.ownerUsername = ownerUsername; this.ownerId = ownerId;
} }
public Long getRecipeId() { public Integer getRecipeId() {
return this.recipeId; return this.recipeId;
} }
public void setRecipeId(Long recipeId) { public void setRecipeId(Integer recipeId) {
this.recipeId = recipeId; this.recipeId = recipeId;
} }
@ -34,19 +34,19 @@ public class RecipeStarId {
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o instanceof RecipeStarId other) { 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; return false;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(this.recipeId, this.ownerUsername); return Objects.hash(this.recipeId, this.ownerId);
} }
@Override @Override
public String toString() { 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> { public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> {
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2") @Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
Optional<RecipeStarEntity> findByRecipeIdAndOwnerUsername(Long recipeId, String username); 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") @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, String viewerUsername); boolean isStarer(String ownerUsername, String slug, Integer viewerId);
@Modifying @Modifying
@Transactional @Transactional
@Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2") @Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerId = ?2")
void deleteByRecipeIdAndOwnerUsername(Long recipeId, String username); void deleteByRecipeIdAndOwnerId(Integer recipeId, Integer ownerId);
} }

View File

@ -6,11 +6,11 @@ import app.mealsmadeeasy.api.user.User;
import java.util.Optional; import java.util.Optional;
public interface RecipeStarService { 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; RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
Optional<RecipeStar> find(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; 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 app.mealsmadeeasy.api.user.User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Optional; import java.util.Optional;
@Service @Service
@ -21,45 +21,45 @@ public class RecipeStarServiceImpl implements RecipeStarService {
} }
@Override @Override
public RecipeStar create(long recipeId, String ownerUsername) { public RecipeStar create(Integer recipeId, Integer ownerId) {
final RecipeStarEntity draft = new RecipeStarEntity(); final RecipeStarEntity draft = new RecipeStarEntity();
final RecipeStarId id = new RecipeStarId(); final RecipeStarId id = new RecipeStarId();
id.setRecipeId(recipeId); id.setRecipeId(recipeId);
id.setOwnerUsername(ownerUsername); id.getOwnerId(ownerId);
draft.setId(id); draft.setId(id);
draft.setDate(LocalDateTime.now()); draft.setTimestamp(OffsetDateTime.now());
return this.recipeStarRepository.save(draft); return this.recipeStarRepository.save(draft);
} }
@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.findByRecipeIdAndOwnerUsername( final Optional<RecipeStarEntity> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
recipe.getId(), recipe.getId(),
starer.getUsername() starer.getId()
); );
if (existing.isPresent()) { if (existing.isPresent()) {
return existing.get(); return existing.get();
} }
return this.create(recipe.getId(), starer.getUsername()); return this.create(recipe.getId(), starer.getId());
} }
@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.findByRecipeIdAndOwnerUsername(recipe.getId(), starer.getUsername()) return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId())
.map(RecipeStar.class::cast); .map(RecipeStar.class::cast);
} }
@Override @Override
public void delete(long recipeId, String ownerUsername) { public void delete(Integer recipeId, Integer ownerId) {
this.recipeStarRepository.deleteByRecipeIdAndOwnerUsername(recipeId, ownerUsername); this.recipeStarRepository.deleteByRecipeIdAndOwnerId(recipeId, ownerId);
} }
@Override @Override
public void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException { public void delete(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);
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 com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public class FullRecipeView { public class FullRecipeView {
@ -41,8 +41,8 @@ public class FullRecipeView {
} }
private long id; private long id;
private LocalDateTime created; private OffsetDateTime created;
private @Nullable LocalDateTime modified; private @Nullable OffsetDateTime modified;
private String slug; private String slug;
private String title; private String title;
private @Nullable Integer preparationTime; private @Nullable Integer preparationTime;
@ -64,19 +64,19 @@ public class FullRecipeView {
this.id = id; this.id = id;
} }
public LocalDateTime getCreated() { public OffsetDateTime getCreated() {
return this.created; return this.created;
} }
public void setCreated(LocalDateTime created) { public void setCreated(OffsetDateTime created) {
this.created = created; this.created = created;
} }
public @Nullable LocalDateTime getModified() { public @Nullable OffsetDateTime getModified() {
return this.modified; return this.modified;
} }
public void setModified(@Nullable LocalDateTime modified) { public void setModified(@Nullable OffsetDateTime modified) {
this.modified = modified; this.modified = modified;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
spring.application.name=meals-made-easy-api 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.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.username=${POSTGRES_USER:meals-made-easy-api-user}
spring.datasource.password=${POSTGRES_PASSWORD} 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(),
recipeStar -> ((RecipeStarEntity) recipeStar).getId(), recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId()) (id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerUsername(), id1.getOwnerUsername()) && Objects.equals(id0.getOwnerId(), id1.getOwnerId())
); );
} }