Move backend files into a sub folder.

This commit is contained in:
Florian THIERRY
2024-03-27 10:28:22 +01:00
parent 39663e914d
commit 431d365d20
131 changed files with 3633 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
package org.codiki.infrastructure.category;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.domain.category.exception.CategoryNotFoundException;
import org.codiki.domain.category.model.Category;
import org.codiki.domain.category.port.CategoryPort;
import org.codiki.infrastructure.category.model.CategoryEntity;
import org.codiki.infrastructure.category.repository.CategoryRepository;
import org.springframework.stereotype.Component;
@Component
public class CategoryJpaAdapter implements CategoryPort {
private final CategoryRepository categoryRepository;
public CategoryJpaAdapter(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@Override
public Optional<Category> findById(UUID categoryId) {
return categoryRepository.findById(categoryId)
.map(CategoryEntity::toDomain);
}
@Override
public void save(Category category) {
CategoryEntity categoryEntity = new CategoryEntity(category);
categoryRepository.save(categoryEntity);
}
@Override
public List<Category> findAllByIds(List<UUID> categoryIds) {
final List<Category> categories = categoryRepository.findAllById(categoryIds)
.stream()
.map(CategoryEntity::toDomain)
.toList();
Optional<UUID> notFoundCategoryId = categoryIds.stream()
.filter(categoryId -> categories.stream().map(Category::id).noneMatch(categoryId::equals))
.findFirst();
if (notFoundCategoryId.isPresent()) {
throw new CategoryNotFoundException(notFoundCategoryId.get());
}
return categories;
}
@Override
public boolean existsAnyAssociatedPublication(UUID categoryId) {
return categoryRepository.existsAnyAssociatedPublication(categoryId);
}
@Override
public void deleteById(UUID categoryId) {
categoryRepository.deleteById(categoryId);
}
@Override
public boolean existsById(UUID categoryId) {
return categoryRepository.existsById(categoryId);
}
@Override
public List<Category> findAll() {
return categoryRepository.findAll()
.stream()
.map(CategoryEntity::toDomain)
.toList();
}
}

View File

@@ -0,0 +1,63 @@
package org.codiki.infrastructure.category.model;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.codiki.domain.category.model.Category;
import static jakarta.persistence.CascadeType.ALL;
import static jakarta.persistence.FetchType.LAZY;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "category")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CategoryEntity {
@Id
private UUID id;
@Column(nullable = false)
private String name;
@OneToMany
@JoinColumn(name = "parent_category_id")
private Set<CategoryEntity> subCategories;
public CategoryEntity(Category category) {
this(
category.id(),
category.name(),
category.subCategories()
.stream()
.map(CategoryEntity::new)
.collect(toSet())
);
}
public Category toDomain() {
return new Category(
id,
name,
subCategories.stream()
.map(CategoryEntity::toDomain)
.toList()
);
}
}

View File

@@ -0,0 +1,19 @@
package org.codiki.infrastructure.category.repository;
import java.util.UUID;
import org.codiki.infrastructure.category.model.CategoryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface CategoryRepository extends JpaRepository<CategoryEntity, UUID> {
@Query(value = """
SELECT (
SELECT COUNT(*)
FROM publication p
WHERE p.category_id = :categoryId
) > 0
""", nativeQuery = true)
boolean existsAnyAssociatedPublication(@Param("categoryId") UUID categoryId);
}

View File

@@ -0,0 +1,11 @@
package org.codiki.infrastructure.configuration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories("org.codiki.infrastructure")
@EntityScan("org.codiki.infrastructure")
public class JpaConfiguration {
}

View File

@@ -0,0 +1,69 @@
package org.codiki.infrastructure.picture;
import java.io.File;
import java.util.Optional;
import java.util.UUID;
import static org.codiki.domain.picture.model.builder.PictureBuilder.aPicture;
import org.codiki.domain.picture.exception.PictureNotFoundException;
import org.codiki.domain.picture.exception.PictureStorageErrorException;
import org.codiki.domain.picture.model.Picture;
import org.codiki.domain.picture.port.PicturePort;
import org.codiki.infrastructure.picture.model.PictureEntity;
import org.codiki.infrastructure.picture.repository.PictureRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional;
@Component
public class PictureJpaAdapter implements PicturePort {
private final PictureRepository repository;
private final String pictureFolderPath;
public PictureJpaAdapter(
PictureRepository repository,
@Value("${application.pictures.path}") String pictureFolderPath
) {
this.repository = repository;
this.pictureFolderPath = pictureFolderPath;
}
@Override
public boolean existsById(UUID pictureId) {
return repository.existsById(pictureId);
}
@Override
public Optional<Picture> findById(UUID pictureId) {
return repository.findById(pictureId)
.map(PictureEntity::toDomain)
.map(picture -> {
File pictureFile = new File(String.format("%s/%s", pictureFolderPath, pictureId));
if (!pictureFile.exists()) {
throw new PictureNotFoundException(pictureId);
}
return aPicture()
.basedOn(picture)
.withContentFile(pictureFile)
.build();
});
}
@Override
@Transactional
public void save(Picture picture) {
PictureEntity pictureEntity = new PictureEntity(picture);
repository.save(pictureEntity);
boolean isMoved = picture.contentFile().renameTo(new File(String.format("%s/%s", pictureFolderPath, picture.id())));
if (!isMoved) {
throw new PictureStorageErrorException();
}
}
@Override
public void deleteById(UUID pictureId) {
repository.deleteById(pictureId);
}
}

View File

@@ -0,0 +1,36 @@
package org.codiki.infrastructure.picture.model;
import java.util.UUID;
import org.codiki.domain.picture.model.Picture;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "picture")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class PictureEntity {
@Id
private UUID id;
@Column(nullable = false)
private UUID publisherId;
public PictureEntity(Picture picture) {
id = picture.id();
publisherId = picture.publisherId();
}
public Picture toDomain() {
return new Picture(id, publisherId, null);
}
}

View File

@@ -0,0 +1,9 @@
package org.codiki.infrastructure.picture.repository;
import java.util.UUID;
import org.codiki.infrastructure.picture.model.PictureEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PictureRepository extends JpaRepository<PictureEntity, UUID> {
}

View File

@@ -0,0 +1,58 @@
package org.codiki.infrastructure.publication;
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparingInt;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.domain.publication.port.PublicationPort;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.codiki.infrastructure.publication.model.PublicationSearchResult;
import org.codiki.infrastructure.publication.repository.PublicationRepository;
import org.springframework.stereotype.Component;
@Component
public class PublicationJpaAdapter implements PublicationPort {
private final PublicationRepository repository;
private final PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter;
public PublicationJpaAdapter(
PublicationRepository repository,
PublicationSearchCriteriaJpaAdapter publicationSearchCriteriaJpaAdapter
) {
this.repository = repository;
this.publicationSearchCriteriaJpaAdapter = publicationSearchCriteriaJpaAdapter;
}
@Override
public void save(Publication publication) {
PublicationEntity newPublicationEntity = new PublicationEntity(publication);
repository.save(newPublicationEntity);
}
@Override
public Optional<Publication> findById(UUID publicationId) {
return repository.findById(publicationId)
.map(PublicationEntity::toDomain);
}
@Override
public void delete(Publication publication) {
repository.deleteById(publication.id());
}
@Override
public List<Publication> search(List<PublicationSearchCriterion> criteria) {
List<PublicationSearchCriterion> adaptedCriteria = publicationSearchCriteriaJpaAdapter.adaptCriteriaForJpa(criteria);
return repository.search(adaptedCriteria)
.stream()
.map(PublicationEntity::toDomain)
.map(publication -> new PublicationSearchResult(publication, criteria))
.sorted(reverseOrder(comparingInt(PublicationSearchResult::getSearchScore)))
.map(PublicationSearchResult::getPublication)
.toList();
}
}

View File

@@ -0,0 +1,34 @@
package org.codiki.infrastructure.publication;
import java.util.LinkedList;
import java.util.List;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.springframework.stereotype.Component;
@Component
public class PublicationSearchCriteriaJpaAdapter {
public List<PublicationSearchCriterion> adaptCriteriaForJpa(List<PublicationSearchCriterion> initialCriteria) {
List<PublicationSearchCriterion> result = new LinkedList<>();
for (PublicationSearchCriterion criterion : initialCriteria) {
boolean criterionAdaptationOccurred = false;
if (criterion.value() instanceof String criterionValue) {
String unaccentedCriterionValue = criterionValue.replaceAll("[àáâãäåçèéêëìíîïñòóôõöùúûüýÿ]", "_");
result.add(new PublicationSearchCriterion(
criterion.searchField(),
criterion.searchType(),
unaccentedCriterionValue.toLowerCase()
));
criterionAdaptationOccurred = true;
}
if (!criterionAdaptationOccurred) {
result.add(criterion);
}
}
return result;
}
}

View File

@@ -0,0 +1,40 @@
package org.codiki.infrastructure.publication.model;
import java.util.UUID;
import org.codiki.domain.publication.model.Author;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "`user`")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuthorEntity {
@Id
private UUID id;
@Column(nullable = false)
private String pseudo;
// private String illustrationId;
public AuthorEntity(Author author) {
this(
author.id(),
author.name()
// author.illustrationId()
);
}
public Author toDomain() {
return new Author(id, pseudo, "image");
}
}

View File

@@ -0,0 +1,75 @@
package org.codiki.infrastructure.publication.model;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.codiki.domain.publication.model.Publication;
import org.codiki.infrastructure.category.model.CategoryEntity;
import static jakarta.persistence.FetchType.LAZY;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "publication")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PublicationEntity {
@Id
private UUID id;
@Column(nullable = false)
private String key;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String text;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private ZonedDateTime creationDate;
@Column(nullable = false)
private UUID illustrationId;
@Column(nullable = false)
private UUID categoryId;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "author_id")
private AuthorEntity author;
public PublicationEntity(Publication publication) {
this(
publication.id(),
publication.key(),
publication.title(),
publication.text(),
publication.description(),
publication.creationDate(),
publication.illustrationId(),
publication.categoryId(),
new AuthorEntity(publication.author())
);
}
public Publication toDomain() {
return new Publication(
id,
key,
title,
text,
description,
creationDate,
illustrationId,
categoryId,
author.toDomain()
);
}
}

View File

@@ -0,0 +1,117 @@
package org.codiki.infrastructure.publication.model;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static org.codiki.domain.publication.model.search.ComparisonType.CONTAINS;
import static org.codiki.domain.publication.model.search.PublicationSearchField.DESCRIPTION;
import static org.codiki.domain.publication.model.search.PublicationSearchField.TEXT;
import static org.codiki.domain.publication.model.search.PublicationSearchField.TITLE;
import org.codiki.domain.publication.model.Publication;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.domain.publication.model.search.PublicationSearchField;
import lombok.Getter;
@Getter
public class PublicationSearchResult {
private final Publication publication;
private final int searchScore;
public PublicationSearchResult(
Publication publication,
List<PublicationSearchCriterion> criteria
) {
this.publication = publication;
int score = 0;
score += computeCriterionScoreForField(TITLE, publication, criteria, 1000, 10, 3);
score += computeCriterionScoreForField(DESCRIPTION, publication, criteria, 100, 7, 2);
score += computeCriterionScoreForField(TEXT, publication, criteria, 100, 4, 1);
searchScore = score;
}
private static int computeCriterionScoreForField(
PublicationSearchField field,
Publication publication,
List<PublicationSearchCriterion> criteria,
int bountyForPerfectMatch,
int bountyForWordMatch,
int bountyForWordContaingMatch
) {
return getLowercaseFieldValue(field, publication)
.map(fieldValue ->
criteria.stream()
.filter(criterion -> criterion.searchType() == CONTAINS)
.filter(criterion -> criterion.searchField() == field)
.map(PublicationSearchCriterion::value)
.filter(String.class::isInstance)
.map(String.class::cast)
.map(String::toLowerCase)
.map(criterionValue ->
computeCriterionScore(
criterionValue,
fieldValue,
bountyForPerfectMatch,
bountyForWordMatch,
bountyForWordContaingMatch
)
)
.mapToInt(Integer::intValue)
.sum()
)
.orElse(0);
}
private static Optional<String> getLowercaseFieldValue(PublicationSearchField field, Publication publication) {
return Optional.ofNullable(
switch (field) {
case TITLE -> publication.title();
case DESCRIPTION -> publication.description();
case TEXT -> publication.text();
default -> null;
}
).map(String::toLowerCase);
}
private static int computeCriterionScore(
String criterionValue,
String publicationTitle,
int bountyForPerfectMatch,
int bountyForWordMatch,
int bountyForWordContaingMatch
) {
int result = 0;
if (publicationTitle.equals(criterionValue)) {
result = bountyForPerfectMatch;
} else if (publicationTitle.contains(criterionValue)) {
result = Stream.of(publicationTitle.split(" "))
.map(titleWord -> computeWordScore(criterionValue, titleWord, bountyForWordMatch, bountyForWordContaingMatch))
.mapToInt(Integer::intValue)
.sum();
}
return result;
}
private static int computeWordScore(
String criterionValue,
String titleWord,
int bountyForWordMatch,
int bountyForWordContaingMatch
) {
int result = 0;
if (titleWord.equals(criterionValue)) {
result = bountyForWordMatch;
} else if (titleWord.contains(criterionValue)) {
result = bountyForWordContaingMatch;
}
return result;
}
}

View File

@@ -0,0 +1,10 @@
package org.codiki.infrastructure.publication.repository;
import java.util.List;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.infrastructure.publication.model.PublicationEntity;
public interface CustomPublicationRepository {
List<PublicationEntity> search(List<PublicationSearchCriterion> criteria);
}

View File

@@ -0,0 +1,41 @@
package org.codiki.infrastructure.publication.repository;
import java.util.List;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
@Repository
public class CustomPublicationRepositoryImpl implements CustomPublicationRepository {
private final EntityManager entityManager;
private final PublicationPredicateMapper publicationPredicateMapper;
public CustomPublicationRepositoryImpl(EntityManager entityManager, PublicationPredicateMapper publicationPredicateMapper) {
this.entityManager = entityManager;
this.publicationPredicateMapper = publicationPredicateMapper;
}
@Override
public List<PublicationEntity> search(final List<PublicationSearchCriterion> criteria) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<PublicationEntity> query = criteriaBuilder.createQuery(PublicationEntity.class);
Root<PublicationEntity> fromPublication = query.from(PublicationEntity.class);
Predicate predicate = publicationPredicateMapper.map(criteria, criteriaBuilder, fromPublication);
CriteriaQuery<PublicationEntity> criteriaQuery = query.select(fromPublication)
.distinct(true)
.where(predicate);
return entityManager.createQuery(criteriaQuery)
.getResultList();
}
}

View File

@@ -0,0 +1,83 @@
package org.codiki.infrastructure.publication.repository;
import java.util.List;
import static org.codiki.domain.publication.model.search.PublicationSearchField.AUTHOR_PSEUDO;
import org.codiki.domain.publication.model.search.PublicationSearchCriterion;
import org.codiki.domain.publication.model.search.PublicationSearchField;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.springframework.stereotype.Component;
import static jakarta.persistence.criteria.JoinType.LEFT;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
@Component
public class PublicationPredicateMapper {
public Predicate map(
List<PublicationSearchCriterion> criteria,
CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication
) {
List<Predicate> criteriaPredicates = criteria.stream()
.map(criterion -> map(criterion, criteriaBuilder, fromPublication))
.toList();
return criteriaBuilder.or(criteriaPredicates.toArray(new Predicate[]{}));
}
private Predicate map(
PublicationSearchCriterion criterion,
CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication
) {
return switch (criterion.searchType()) {
case EQUALS -> mapEqualsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value());
case CONTAINS -> mapContainsPredicate(criteriaBuilder, fromPublication, criterion.searchField(), criterion.value());
default -> null;
};
}
private Predicate mapEqualsPredicate(
CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication,
PublicationSearchField searchField,
Object value
) {
From<?, ?> from = fromPublication;
String attributeName = searchField.name().toLowerCase();
if (searchField == AUTHOR_PSEUDO) {
from = fromPublication.join("author", LEFT);
attributeName = "pseudo";
}
return criteriaBuilder.equal(
criteriaBuilder.lower(
from.get(attributeName)
),
value
);
}
private Predicate mapContainsPredicate(
CriteriaBuilder criteriaBuilder,
Root<PublicationEntity> fromPublication,
PublicationSearchField searchField,
Object value
) {
From<?, ?> from = fromPublication;
String attributeName = searchField.name().toLowerCase();
if (searchField == AUTHOR_PSEUDO) {
from = fromPublication.join("author", LEFT);
attributeName = "pseudo";
}
return criteriaBuilder.like(
criteriaBuilder.lower(
from.get(attributeName)
),
String.format("%%%s%%", value)
);
}
}

View File

@@ -0,0 +1,19 @@
package org.codiki.infrastructure.publication.repository;
import java.util.Optional;
import java.util.UUID;
import org.codiki.infrastructure.publication.model.PublicationEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PublicationRepository extends JpaRepository<PublicationEntity, UUID>, CustomPublicationRepository {
@Query("""
SELECT p
FROM PublicationEntity p
JOIN FETCH p.author a
WHERE p.id = :publicationId
""")
Optional<PublicationEntity> findById(@Param("publicationId") UUID publicationId);
}

View File

@@ -0,0 +1,82 @@
package org.codiki.infrastructure.user.adapter;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.domain.user.model.RefreshToken;
import org.codiki.domain.user.model.User;
import org.codiki.domain.user.port.UserPort;
import org.codiki.infrastructure.user.model.RefreshTokenEntity;
import org.codiki.infrastructure.user.repository.RefreshTokenJpaRepository;
import org.codiki.infrastructure.user.repository.UserJpaRepository;
import org.codiki.infrastructure.user.model.UserEntity;
import org.springframework.stereotype.Component;
@Component
public class UserJpaAdapter implements UserPort {
private final RefreshTokenJpaRepository refreshTokenJpaRepository;
private final UserJpaRepository userJpaRepository;
public UserJpaAdapter(
RefreshTokenJpaRepository refreshTokenJpaRepository,
UserJpaRepository userJpaRepository
) {
this.refreshTokenJpaRepository = refreshTokenJpaRepository;
this.userJpaRepository = userJpaRepository;
}
@Override
public Optional<User> findById(UUID userId) {
return userJpaRepository.findById(userId)
.map(UserEntity::toUser);
}
@Override
public Optional<User> findByEmail(String userEmail) {
return userJpaRepository.findByEmail(userEmail)
.map(UserEntity::toUser);
}
@Override
public List<User> findAll() {
return userJpaRepository.findAll()
.stream()
.map(UserEntity::toUser)
.toList();
}
@Override
public void save(User user) {
UserEntity userEntity = new UserEntity(user);
userJpaRepository.save(userEntity);
}
@Override
public boolean existsById(UUID userId) {
return userJpaRepository.existsById(userId);
}
@Override
public boolean existsByEmail(String email) {
return userJpaRepository.existsByEmail(email);
}
@Override
public Optional<RefreshToken> findRefreshTokenByUserId(UUID userId) {
return refreshTokenJpaRepository.findByUserId(userId)
.map(RefreshTokenEntity::toRefreshToken);
}
@Override
public Optional<RefreshToken> findRefreshTokenById(UUID refreshTokenId) {
return refreshTokenJpaRepository.findByValue(refreshTokenId)
.map(RefreshTokenEntity::toRefreshToken);
}
@Override
public void save(RefreshToken refreshToken) {
RefreshTokenEntity refreshTokenEntity = new RefreshTokenEntity(refreshToken);
refreshTokenJpaRepository.save(refreshTokenEntity);
}
}

View File

@@ -0,0 +1,40 @@
package org.codiki.infrastructure.user.model;
import java.time.ZonedDateTime;
import java.util.UUID;
import org.codiki.domain.user.model.RefreshToken;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "refresh_token")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class RefreshTokenEntity {
@Id
private UUID userId;
@Column(nullable = false)
private UUID value;
@Column(nullable = false)
private ZonedDateTime expirationDate;
public RefreshTokenEntity(RefreshToken refreshToken) {
userId = refreshToken.userId();
value = refreshToken.value();
expirationDate = refreshToken.expirationDate();
}
public RefreshToken toRefreshToken() {
return new RefreshToken(userId, value, expirationDate);
}
}

View File

@@ -0,0 +1,65 @@
package org.codiki.infrastructure.user.model;
import java.util.List;
import java.util.UUID;
import org.codiki.domain.user.model.User;
import org.codiki.domain.user.model.UserRole;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "`user`")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserEntity {
@Id
private UUID id;
@Column(nullable = false)
private String pseudo;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column
private UUID photoId;
@ElementCollection(targetClass = UserRole.class)
@CollectionTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id")
)
@Column(name = "role")
private List<UserRole> roles;
public UserEntity(User user) {
id = user.id();
pseudo = user.pseudo();
email = user.email();
password = user.password();
photoId = user.photoId();
roles = user.roles();
}
public User toUser() {
return new User(
id,
pseudo,
email,
password,
photoId,
roles
);
}
}

View File

@@ -0,0 +1,15 @@
package org.codiki.infrastructure.user.repository;
import java.util.Optional;
import java.util.UUID;
import org.codiki.infrastructure.user.model.RefreshTokenEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RefreshTokenJpaRepository extends JpaRepository<RefreshTokenEntity, UUID> {
Optional<RefreshTokenEntity> findByUserId(UUID userId);
Optional<RefreshTokenEntity> findByValue(UUID refreshTokenId);
}

View File

@@ -0,0 +1,25 @@
package org.codiki.infrastructure.user.repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.codiki.infrastructure.user.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface UserJpaRepository extends JpaRepository<UserEntity, UUID> {
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles WHERE u.id = :userId")
Optional<UserEntity> findById(@Param("userId") UUID userId);
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles WHERE u.email = :email")
Optional<UserEntity> findByEmail(@Param("email") String userEmail);
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles")
List<UserEntity> findAll();
boolean existsByEmail(String email);
}

View File

@@ -0,0 +1,12 @@
insert into "user" values
('5ad462b8-8f9e-4a26-bb86-c74fef5d11b6', 'Standard user', 'standard.user@codiki.org', '$2a$10$FVhrYRXw.Zw2V5jGUkvX/.1U.IdWlwd8J.Y/5pb5etAzyoBhJ3FHG', null),
('15a13dc7-029d-4eab-a63d-c1e96f90241d', 'Admin user', 'admin.user@codiki.org', '$2a$10$FVhrYRXw.Zw2V5jGUkvX/.1U.IdWlwd8J.Y/5pb5etAzyoBhJ3FHG', null);
insert into user_role values
('5ad462b8-8f9e-4a26-bb86-c74fef5d11b6', 0),
('15a13dc7-029d-4eab-a63d-c1e96f90241d', 0),
('15a13dc7-029d-4eab-a63d-c1e96f90241d', 1);
insert into category values
('172fa901-3f4b-4540-92f3-1c15820e8ec9', 'Main category', null),
('3f4b4540-a901-92f3-1c15-8ec9172f820e', 'Sub category', '172fa901-3f4b-4540-92f3-1c15820e8ec9');

View File

@@ -0,0 +1,12 @@
\c codiki_db
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE USER codiki_user
WITH PASSWORD 'password'
NOCREATEDB;
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES
IN SCHEMA public
TO codiki_user;

View File

@@ -0,0 +1,65 @@
CREATE TABLE IF NOT EXISTS "user" (
id UUID NOT NULL,
pseudo VARCHAR NOT NULL,
email VARCHAR NOT NULL,
password VARCHAR NOT NULL,
photo_id UUID,
CONSTRAINT user_pk PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS user_role (
user_id UUID NOT NULL,
role SMALLINT,
CONSTRAINT user_role_pk PRIMARY KEY (user_id, role),
CONSTRAINT user_role_fk_user_id FOREIGN KEY (user_id) REFERENCES "user" (id)
);
CREATE INDEX user_role_fk_user_id_idx ON user_role (user_id);
CREATE TABLE IF NOT EXISTS refresh_token (
user_id UUID NOT NULL,
value UUID NOT NULL,
expiration_date TIMESTAMP NOT NULL,
CONSTRAINT refresh_token_pk PRIMARY KEY (user_id),
CONSTRAINT refresh_token_fk_user_id FOREIGN KEY (user_id) REFERENCES "user" (id)
);
CREATE INDEX refresh_token_fk_user_id_idx ON user_role (user_id);
CREATE TABLE IF NOT EXISTS category (
id UUID NOT NULL,
name VARCHAR NOT NULL,
parent_category_id UUID,
CONSTRAINT category_pk PRIMARY KEY (id),
CONSTRAINT category_parent_category_id_fk FOREIGN KEY (parent_category_id) REFERENCES category (id)
);
CREATE INDEX category_parent_category_id_idx ON category (parent_category_id);
CREATE TABLE IF NOT EXISTS picture (
id UUID NOT NULL,
publisher_id UUID NOT NULL,
CONSTRAINT picture_pk PRIMARY KEY (id),
CONSTRAINT picture_publisher_id_fk FOREIGN KEY (publisher_id) REFERENCES "user" (id)
);
CREATE INDEX picture_publisher_id_idx ON picture (publisher_id);
ALTER TABLE "user" ADD CONSTRAINT user_photo_id_fk FOREIGN KEY (photo_id) REFERENCES picture (id);
CREATE INDEX user_photo_id_idx ON "user" (photo_id);
CREATE TABLE IF NOT EXISTS publication (
id UUID NOT NULL,
key VARCHAR(14) NOT NULL,
title VARCHAR NOT NULL,
text VARCHAR NOT NULL,
description VARCHAR NOT NULL,
creation_date TIMESTAMP NOT NULL,
illustration_id UUID NOT NULL,
author_id UUID NOT NULL,
category_id UUID NOT NULL,
CONSTRAINT publication_pk PRIMARY KEY (id),
CONSTRAINT publication_picture_id_fk FOREIGN KEY (illustration_id) REFERENCES picture (id),
CONSTRAINT publication_author_id_fk FOREIGN KEY (author_id) REFERENCES "user" (id),
CONSTRAINT publication_category_id_fk FOREIGN KEY (category_id) REFERENCES category (id)
);
CREATE INDEX publication_picture_id_idx ON publication (illustration_id);
CREATE INDEX publication_author_id_idx ON publication (author_id);
CREATE INDEX publication_category_id_idx ON publication (category_id);