Implemented follower logic

This commit is contained in:
2026-01-11 13:01:00 +01:00
parent 2899671321
commit b18e21d4c7
14 changed files with 515 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
);