Implemented follower logic
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package dev.mednikov.social.connections.config;
|
||||
|
||||
import dev.mednikov.social.connections.repositories.IdentifierGenerator;
|
||||
import dev.mednikov.social.connections.repositories.IdentifierGeneratorImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class IdentifierGeneratorConfig {
|
||||
|
||||
@Bean
|
||||
public IdentifierGenerator identifierGenerator() {
|
||||
return new IdentifierGeneratorImpl();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dev.mednikov.social.connections.domain;
|
||||
|
||||
public final class CreateFollowerRequestDto {
|
||||
|
||||
private String ownerId;
|
||||
private String followedUserId;
|
||||
|
||||
public String getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwnerId(String ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
}
|
||||
|
||||
public String getFollowedUserId() {
|
||||
return followedUserId;
|
||||
}
|
||||
|
||||
public void setFollowedUserId(String followedUserId) {
|
||||
this.followedUserId = followedUserId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.mednikov.social.connections.domain;
|
||||
|
||||
public final class FollowerDto {
|
||||
|
||||
private String id;
|
||||
private String ownerId;
|
||||
private UserDto followedUser;
|
||||
private boolean muted;
|
||||
private boolean mutual;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOwnerId() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
public void setOwnerId(String ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
}
|
||||
|
||||
public UserDto getFollowedUser() {
|
||||
return followedUser;
|
||||
}
|
||||
|
||||
public void setFollowedUser(UserDto followedUser) {
|
||||
this.followedUser = followedUser;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return muted;
|
||||
}
|
||||
|
||||
public void setMuted(boolean muted) {
|
||||
this.muted = muted;
|
||||
}
|
||||
|
||||
public boolean isMutual() {
|
||||
return mutual;
|
||||
}
|
||||
|
||||
public void setMutual(boolean mutual) {
|
||||
this.mutual = mutual;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dev.mednikov.social.connections.domain;
|
||||
|
||||
import dev.mednikov.social.connections.models.Follower;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class FollowerDtoMapper implements Function<Follower, FollowerDto> {
|
||||
|
||||
private final static UserDtoMapper userDtoMapper = new UserDtoMapper();
|
||||
|
||||
@Override
|
||||
public FollowerDto apply(Follower follower) {
|
||||
FollowerDto result = new FollowerDto();
|
||||
result.setId(follower.getId().toString());
|
||||
result.setOwnerId(follower.getOwner().getId().toString());
|
||||
result.setFollowedUser(userDtoMapper.apply(follower.getFollowedUser()));
|
||||
result.setMuted(follower.getMuted());
|
||||
result.setMutual(follower.getMutual());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.mednikov.social.connections.domain;
|
||||
|
||||
public final class UserDto {
|
||||
|
||||
private String id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String email;
|
||||
private boolean active;
|
||||
private boolean onboarded;
|
||||
private boolean emailVerified;
|
||||
private String avatarUrl;
|
||||
private String headline;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public boolean isOnboarded() {
|
||||
return onboarded;
|
||||
}
|
||||
|
||||
public void setOnboarded(boolean onboarded) {
|
||||
this.onboarded = onboarded;
|
||||
}
|
||||
|
||||
public boolean isEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getHeadline() {
|
||||
return headline;
|
||||
}
|
||||
|
||||
public void setHeadline(String headline) {
|
||||
this.headline = headline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.mednikov.social.connections.domain;
|
||||
|
||||
import dev.mednikov.social.connections.models.User;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class UserDtoMapper implements Function<User, UserDto> {
|
||||
|
||||
@Override
|
||||
public UserDto apply(User user) {
|
||||
UserDto result = new UserDto();
|
||||
result.setId(user.getId().toString());
|
||||
result.setFirstName(user.getFirstName());
|
||||
result.setLastName(user.getLastName());
|
||||
result.setEmail(user.getEmail());
|
||||
result.setHeadline(user.getHeadline());
|
||||
result.setAvatarUrl(user.getAvatarUrl());
|
||||
result.setActive(user.getActive());
|
||||
result.setEmailVerified(user.getEmailVerified());
|
||||
result.setOnboarded(user.getOnboarded());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package dev.mednikov.social.connections.exceptions;
|
||||
|
||||
public class FollowerAlreadyExistsException extends ObjectAlreadyExistsException{
|
||||
|
||||
public FollowerAlreadyExistsException(Long ownerId, Long followedId) {
|
||||
super("Follower between " + ownerId + " and " + followedId + " already exists");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package dev.mednikov.social.connections.exceptions;
|
||||
|
||||
public class UserDoesNotExistException extends ObjectDoesNotExistException{
|
||||
|
||||
public UserDoesNotExistException(Long userId) {
|
||||
super("User " + userId + " does not exist");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package dev.mednikov.social.connections.models;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.OnDelete;
|
||||
import org.hibernate.annotations.OnDeleteAction;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "connections_follower",
|
||||
uniqueConstraints = {@UniqueConstraint(columnNames = {"owner_id", "followed_user_id"})}
|
||||
)
|
||||
public class Follower {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "owner_id", nullable = false)
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
private User owner;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "followed_user_id", nullable = false)
|
||||
@OnDelete(action = OnDeleteAction.CASCADE)
|
||||
private User followedUser;
|
||||
|
||||
@Column(name = "is_mutual", nullable = false)
|
||||
private Boolean mutual;
|
||||
|
||||
@Column(name = "is_muted", nullable = false)
|
||||
private Boolean muted;
|
||||
|
||||
@Column(name = "version", nullable = false)
|
||||
private Long version;
|
||||
|
||||
@Column(name = "created_at")
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
@UpdateTimestamp
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Override
|
||||
public final boolean equals(Object o) {
|
||||
if (!(o instanceof Follower follower)) return false;
|
||||
|
||||
return owner.equals(follower.owner)
|
||||
&& followedUser.equals(follower.followedUser)
|
||||
&& version.equals(follower.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = owner.hashCode();
|
||||
result = 31 * result + followedUser.hashCode();
|
||||
result = 31 * result + version.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(User owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public User getFollowedUser() {
|
||||
return followedUser;
|
||||
}
|
||||
|
||||
public void setFollowedUser(User followedUser) {
|
||||
this.followedUser = followedUser;
|
||||
}
|
||||
|
||||
public Boolean getMutual() {
|
||||
return mutual;
|
||||
}
|
||||
|
||||
public void setMutual(Boolean mutual) {
|
||||
this.mutual = mutual;
|
||||
}
|
||||
|
||||
public Boolean getMuted() {
|
||||
return muted;
|
||||
}
|
||||
|
||||
public void setMuted(Boolean muted) {
|
||||
this.muted = muted;
|
||||
}
|
||||
|
||||
public Long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Long version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package dev.mednikov.social.connections.repositories;
|
||||
|
||||
import dev.mednikov.social.connections.models.Follower;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FollowerRepository extends JpaRepository<Follower, Long> {
|
||||
|
||||
@Query("SELECT f FROM Follower f WHERE f.owner.id = :ownerId AND f.followedUser.id = :followedId")
|
||||
Optional<Follower> findForOwnerAndFollowed (Long ownerId, Long followedId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.mednikov.social.connections.services;
|
||||
|
||||
import dev.mednikov.social.connections.domain.CreateFollowerRequestDto;
|
||||
import dev.mednikov.social.connections.domain.FollowerDto;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FollowerService {
|
||||
|
||||
FollowerDto createFollower(CreateFollowerRequestDto request);
|
||||
|
||||
void deleteFollower (Long id);
|
||||
|
||||
Optional<FollowerDto> findExistingFollower (Long ownerId, Long followedUserId);
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package dev.mednikov.social.connections.services;
|
||||
|
||||
import dev.mednikov.social.connections.domain.CreateFollowerRequestDto;
|
||||
import dev.mednikov.social.connections.domain.FollowerDto;
|
||||
import dev.mednikov.social.connections.domain.FollowerDtoMapper;
|
||||
import dev.mednikov.social.connections.exceptions.FollowerAlreadyExistsException;
|
||||
import dev.mednikov.social.connections.exceptions.UserDoesNotExistException;
|
||||
import dev.mednikov.social.connections.models.Follower;
|
||||
import dev.mednikov.social.connections.models.User;
|
||||
import dev.mednikov.social.connections.repositories.FollowerRepository;
|
||||
import dev.mednikov.social.connections.repositories.IdentifierGenerator;
|
||||
import dev.mednikov.social.connections.repositories.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class FollowerServiceImpl implements FollowerService {
|
||||
|
||||
private final static FollowerDtoMapper mapper = new FollowerDtoMapper();
|
||||
|
||||
private final FollowerRepository followerRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final IdentifierGenerator identifierGenerator;
|
||||
|
||||
public FollowerServiceImpl(
|
||||
FollowerRepository followerRepository,
|
||||
UserRepository userRepository,
|
||||
IdentifierGenerator identifierGenerator) {
|
||||
this.followerRepository = followerRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.identifierGenerator = identifierGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<FollowerDto> findExistingFollower(Long ownerId, Long followedUserId) {
|
||||
return this.followerRepository
|
||||
.findForOwnerAndFollowed(ownerId, followedUserId)
|
||||
.map(mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FollowerDto createFollower(CreateFollowerRequestDto request) {
|
||||
Long ownerId = Long.parseLong(request.getOwnerId());
|
||||
Long followedUserId = Long.parseLong(request.getFollowedUserId());
|
||||
if (this.followerRepository.findForOwnerAndFollowed(ownerId, followedUserId).isPresent()) {
|
||||
throw new FollowerAlreadyExistsException(ownerId, followedUserId);
|
||||
}
|
||||
|
||||
User owner = this.userRepository.findById(ownerId).orElseThrow(() ->
|
||||
new UserDoesNotExistException(ownerId));
|
||||
User followedUser = this.userRepository.findById(followedUserId).orElseThrow(() ->
|
||||
new UserDoesNotExistException(ownerId));
|
||||
|
||||
Follower follower = new Follower();
|
||||
follower.setOwner(owner);
|
||||
follower.setFollowedUser(followedUser);
|
||||
follower.setMuted(false);
|
||||
follower.setMutual(false);
|
||||
follower.setVersion(1L);
|
||||
follower.setId(this.identifierGenerator.getNextId());
|
||||
|
||||
Follower savedFollower = this.followerRepository.save(follower);
|
||||
|
||||
return mapper.apply(savedFollower);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFollower(Long id) {
|
||||
this.followerRepository.deleteById(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package dev.mednikov.social.connections.web;
|
||||
|
||||
import dev.mednikov.social.connections.domain.CreateFollowerRequestDto;
|
||||
import dev.mednikov.social.connections.domain.FollowerDto;
|
||||
import dev.mednikov.social.connections.services.FollowerService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/followers")
|
||||
public class FollowerRestController {
|
||||
|
||||
private final FollowerService followerService;
|
||||
|
||||
public FollowerRestController(FollowerService followerService) {
|
||||
this.followerService = followerService;
|
||||
}
|
||||
|
||||
@GetMapping("/find")
|
||||
public ResponseEntity<FollowerDto> findExistingFollower (
|
||||
@RequestParam(value = "followerId", required = true) String followerId,
|
||||
@RequestParam(value = "followedId", required = true) String followedId
|
||||
) {
|
||||
Long ownerId = Long.parseLong(followerId);
|
||||
Long followedUserId = Long.parseLong(followedId);
|
||||
Optional<FollowerDto> result = this.followerService.findExistingFollower(ownerId, followedUserId);
|
||||
return ResponseEntity.of(result);
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public @ResponseBody FollowerDto createFollower (@RequestBody CreateFollowerRequestDto body) {
|
||||
return this.followerService.createFollower(body);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void deleteFollower (@PathVariable Long id){
|
||||
this.followerService.deleteFollower(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,4 +9,18 @@ CREATE TABLE IF NOT EXISTS connections_user (
|
||||
is_email_verified BOOLEAN NOT NULL,
|
||||
headline VARCHAR(255) NOT NULL,
|
||||
version BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS connections_follower (
|
||||
id BIGINT PRIMARY KEY,
|
||||
owner_id BIGINT NOT NULL,
|
||||
followed_user_id BIGINT NOT NULL,
|
||||
is_mutual BOOLEAN NOT NULL,
|
||||
is_muted BOOLEAN NOT NULL,
|
||||
version BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (owner_id, followed_user_id),
|
||||
CONSTRAINT fk_connections_follower_owner FOREIGN KEY (owner_id) REFERENCES connections_user(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_connections_follower_followed_user FOREIGN KEY (followed_user_id) REFERENCES connections_user(id) ON DELETE CASCADE
|
||||
);
|
||||
Reference in New Issue
Block a user