diff --git a/pom.xml b/pom.xml index 6890ffc..ff263ee 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,14 @@ org.springframework.boot spring-boot-starter - + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + org.postgresql postgresql @@ -45,7 +52,16 @@ spring-boot-starter-test test - + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-web + + @@ -53,6 +69,21 @@ org.springframework.boot spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + + org.projectlombok + lombok + 1.18.42 + + + + diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java new file mode 100644 index 0000000..013246e --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java @@ -0,0 +1,66 @@ +package net.hackyourfuture.tickettrackingsystem.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.dto.request.CreateUserRequest; +import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateUserRequest; +import net.hackyourfuture.tickettrackingsystem.dto.response.UserResponse; +import net.hackyourfuture.tickettrackingsystem.model.User; +import net.hackyourfuture.tickettrackingsystem.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + private UserResponse toUserResponse(User user) { + return new UserResponse( + user.id(), + user.name(), + user.email() + ); + } + @GetMapping + public List getAllUsers() { + return userService.getAllUsers() + .stream() + .map(this::toUserResponse). + toList(); + } + + @GetMapping("/{id}") + public UserResponse getUserById(@PathVariable Long id) { + return toUserResponse( + userService.getUserById(id) + ); + } + + @PostMapping + public ResponseEntity createUser( @Valid @RequestBody CreateUserRequest request) { + User user = userService.createUser(request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(toUserResponse(user)); + } + + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { + User user = userService.updateUser(id, request); + + return ResponseEntity.ok(toUserResponse(user)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java new file mode 100644 index 0000000..50a64d0 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CreateUserRequest( + @NotBlank + @Size(min = 3) + String name, + @NotBlank + @Email + String email) { +} diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java new file mode 100644 index 0000000..f681080 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java @@ -0,0 +1,17 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UpdateUserRequest( + + @NotBlank + @Size(min = 3) + String name, + + @NotBlank + @Email + String email + +) {} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java new file mode 100644 index 0000000..a6e8734 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.dto.response; + +public record UserResponse( + Long id, + String name, + String email +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java new file mode 100644 index 0000000..a7b3025 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +public class DuplicateEmailException extends RuntimeException { + + public DuplicateEmailException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..60d9e34 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java @@ -0,0 +1,28 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFound( + ResourceNotFoundException ex) { + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", ex.getMessage())); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity> handleDuplicateEmail( + DuplicateEmailException ex) { + + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(Map.of("message", ex.getMessage())); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..dc07743 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java new file mode 100644 index 0000000..d22fd58 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.tickettrackingsystem.model; + +public record User(Long id, String name, String email) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java new file mode 100644 index 0000000..c2f03f8 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java @@ -0,0 +1,92 @@ +package net.hackyourfuture.tickettrackingsystem.repository; + +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.model.User; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class UserRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper userRowMapper = (rs, rowNum) -> + new User( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email") + ); + + public List findAll() { + String sql = "SELECT id, name, email FROM users"; + return jdbcTemplate.query(sql, userRowMapper); + } + + public Optional findById(Long id) { + String sql = "SELECT id, name, email FROM users WHERE id = ?"; + + try { + User user = jdbcTemplate.queryForObject(sql, userRowMapper, id); + return Optional.of(user); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + public Optional findByEmail(String email) { + String sql = "SELECT id, name, email FROM users WHERE email = ?"; + + try { + User user = jdbcTemplate.queryForObject(sql, userRowMapper, email); + return Optional.of(user); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + public User create(User user) { + String sql = """ + INSERT INTO users (name, email) + VALUES (?, ?) + RETURNING id + """; + + Long id = jdbcTemplate.queryForObject( + sql, + Long.class, + user.name(), + user.email() + ); + + return new User(id, user.name(), user.email()); + } + + public void update(User user) { + String sql = """ + UPDATE users + SET name = ?, email = ? + WHERE id = ? + """; + + jdbcTemplate.update( + sql, + user.name(), + user.email(), + user.id() + ); + } + + public boolean deleteById(Long id) { + String sql = "DELETE FROM users WHERE id = ?"; + + int rowsAffected = jdbcTemplate.update(sql, id); + + return rowsAffected > 0; + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java new file mode 100644 index 0000000..e04cd4a --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java @@ -0,0 +1,80 @@ +package net.hackyourfuture.tickettrackingsystem.service; + + +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.dto.request.CreateUserRequest; +import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateUserRequest; +import net.hackyourfuture.tickettrackingsystem.exception.DuplicateEmailException; +import net.hackyourfuture.tickettrackingsystem.model.User; +import net.hackyourfuture.tickettrackingsystem.repository.UserRepository; +import org.springframework.stereotype.Service; +import net.hackyourfuture.tickettrackingsystem.exception.ResourceNotFoundException; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User createUser(CreateUserRequest request) { + if (userRepository.findByEmail(request.email()).isPresent()) { + throw new DuplicateEmailException( + "Email " + request.email() + " already exists"); + } + return userRepository.create(new User( + null, + request.name(), + request.email() + )); + } + + public List getAllUsers() { + return userRepository.findAll(); + } + + public User getUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> + new ResourceNotFoundException( + "User with id " + id + " not found")); + } + + public void deleteUser(Long id) { + boolean deleted = userRepository.deleteById(id); + + if (!deleted) { + throw new ResourceNotFoundException( + "User with id " + id + " not found"); + } + } + + public User updateUser(Long id, UpdateUserRequest request) { + + User existingUser = userRepository.findById(id) + .orElseThrow(() -> + new ResourceNotFoundException( + "User with id " + id + " not found")); + + Optional userWithSameEmail = + userRepository.findByEmail(request.email()); + + if (userWithSameEmail.isPresent() + && !userWithSameEmail.get().id().equals(id)) { + throw new DuplicateEmailException( + "Email " + request.email() + " already exists"); + } + + User updatedUser = new User( + existingUser.id(), + request.name(), + request.email() + ); + + userRepository.update(updatedUser); + + return updatedUser; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26d4cb5..3145487 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=ticket-tracking-system + +spring.datasource.url=jdbc:postgresql://localhost:5432/ticket_tracker +spring.datasource.username=ticket_user +spring.datasource.password=ticket_password +spring.datasource.driver-class-name=org.postgresql.Driver + diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..317d08b --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,51 @@ +CREATE TYPE ticket_status AS ENUM ( + 'OPEN', 'IN_PROGRESS', 'CLOSED'); + +CREATE TABLE users ( + id bigserial primary key , + name varchar(255) not null CHECK (char_length(name) >= 3), + email varchar(255) not null unique + +); +CREATE TABLE projects ( + id bigserial primary key , + name varchar(255) not null,CHECK (char_length(name) >= 3) +); +CREATE TABLE tickets ( + id bigserial primary key , + title varchar(255) not null, + description varchar(255) , + project_id BIGINT NOT NULL, + status ticket_status not null, + creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_date TIMESTAMP, + CONSTRAINT fk_ticket_project + FOREIGN KEY (project_id) + REFERENCES project(id) + +); + +CREATE TABLE user_ticket ( + user_id BIGINT NOT NULL, + ticket_id BIGINT NOT NULL, + + primary key (user_id, ticket_id), + + CONSTRAINT fk_user_ticket_user + FOREIGN KEY (user_id) + REFERENCES users(id) + ON DELETE CASCADE, + CONSTRAINT fk_user_ticket_ticket + FOREIGN KEY (ticket_id) + REFERENCES ticket(id) + ON DELETE CASCADE + ); + +CREATE INDEX idx_tickets_status + ON ticket(status); + +CREATE INDEX idx_tickets_project_id + ON ticket(project_id); + +CREATE INDEX idx_user_ticket_user_id + ON user_ticket(user_id); \ No newline at end of file