This commit is contained in:
roma-dxunvrs
2026-10-03 22:09:58 +03:00
commit 8d4b15dbcf
33 changed files with 1655 additions and 0 deletions
@@ -0,0 +1,13 @@
package ru.dxunvrs.todo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TodoApplication {
public static void main(String[] args) {
SpringApplication.run(TodoApplication.class, args);
}
}
@@ -0,0 +1,45 @@
package ru.dxunvrs.todo.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dxunvrs.todo.dto.TaskCreateRequest;
import ru.dxunvrs.todo.dto.TaskResponse;
import ru.dxunvrs.todo.dto.TaskUpdateRequest;
import ru.dxunvrs.todo.service.TaskService;
@RestController
@RequestMapping("/api/tasks")
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
@GetMapping("/{id}")
public ResponseEntity<TaskResponse> getTask(@PathVariable Long id) {
TaskResponse response = taskService.getById(id);
return ResponseEntity.ok(response);
}
@PostMapping
public ResponseEntity<TaskResponse> createTask(@Valid @RequestBody TaskCreateRequest request) {
TaskResponse response = taskService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PutMapping("/{id}")
public ResponseEntity<TaskResponse> updateTask(
@PathVariable Long id,
@Valid @RequestBody TaskUpdateRequest request) {
TaskResponse response = taskService.update(id, request);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,19 @@
package ru.dxunvrs.todo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskCreateRequest {
@NotBlank(message = "Title is required")
private String title;
private String description;
private Boolean completed;
}
@@ -0,0 +1,20 @@
package ru.dxunvrs.todo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskResponse {
private Long id;
private String title;
private String description;
private Boolean completed;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
package ru.dxunvrs.todo.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskUpdateRequest {
@NotBlank(message = "Title is required")
private String title;
private String description;
private Boolean completed;
}
@@ -0,0 +1,46 @@
package ru.dxunvrs.todo.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "tasks")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false)
private Boolean completed = false;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,58 @@
package ru.dxunvrs.todo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
public ResponseEntity<ErrorResponse> handleTaskNotFound(TaskNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String message = error.getDefaultMessage();
errors.put(fieldName, message);
});
ValidationErrorResponse response = new ValidationErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
LocalDateTime.now(),
errors
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal server error",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
record ErrorResponse(int status, String message, LocalDateTime timestamp) {}
record ValidationErrorResponse(int status, String message, LocalDateTime timestamp, Map<String, String> errors) {}
}
@@ -0,0 +1,7 @@
package ru.dxunvrs.todo.exception;
public class TaskNotFoundException extends RuntimeException {
public TaskNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,38 @@
package ru.dxunvrs.todo.mapper;
import org.springframework.stereotype.Component;
import ru.dxunvrs.todo.dto.TaskCreateRequest;
import ru.dxunvrs.todo.dto.TaskResponse;
import ru.dxunvrs.todo.dto.TaskUpdateRequest;
import ru.dxunvrs.todo.entity.Task;
@Component
public class TaskMapper {
public Task toEntity(TaskCreateRequest request) {
Task task = new Task();
task.setTitle(request.getTitle());
task.setDescription(request.getDescription());
task.setCompleted(request.getCompleted() != null ? request.getCompleted() : false);
return task;
}
public void updateEntity(Task task, TaskUpdateRequest request) {
task.setTitle(request.getTitle());
task.setDescription(request.getDescription());
if (request.getCompleted() != null) {
task.setCompleted(request.getCompleted());
}
}
public TaskResponse toResponse(Task task) {
return new TaskResponse(
task.getId(),
task.getTitle(),
task.getDescription(),
task.getCompleted(),
task.getCreatedAt(),
task.getUpdatedAt()
);
}
}
@@ -0,0 +1,9 @@
package ru.dxunvrs.todo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.dxunvrs.todo.entity.Task;
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
}
@@ -0,0 +1,16 @@
package ru.dxunvrs.todo.service;
import ru.dxunvrs.todo.dto.TaskCreateRequest;
import ru.dxunvrs.todo.dto.TaskResponse;
import ru.dxunvrs.todo.dto.TaskUpdateRequest;
public interface TaskService {
TaskResponse getById(Long id);
TaskResponse create(TaskCreateRequest request);
TaskResponse update(Long id, TaskUpdateRequest request);
void delete(Long id);
}
@@ -0,0 +1,55 @@
package ru.dxunvrs.todo.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.dxunvrs.todo.dto.TaskCreateRequest;
import ru.dxunvrs.todo.dto.TaskResponse;
import ru.dxunvrs.todo.dto.TaskUpdateRequest;
import ru.dxunvrs.todo.entity.Task;
import ru.dxunvrs.todo.exception.TaskNotFoundException;
import ru.dxunvrs.todo.mapper.TaskMapper;
import ru.dxunvrs.todo.repository.TaskRepository;
@Service
@RequiredArgsConstructor
public class TaskServiceImpl implements TaskService {
private final TaskRepository taskRepository;
private final TaskMapper taskMapper;
@Override
@Transactional(readOnly = true)
public TaskResponse getById(Long id) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException("Task not found with id: " + id));
return taskMapper.toResponse(task);
}
@Override
@Transactional
public TaskResponse create(TaskCreateRequest request) {
Task task = taskMapper.toEntity(request);
Task saved = taskRepository.save(task);
return taskMapper.toResponse(saved);
}
@Override
@Transactional
public TaskResponse update(Long id, TaskUpdateRequest request) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException("Task not found with id: " + id));
taskMapper.updateEntity(task, request);
Task updated = taskRepository.save(task);
return taskMapper.toResponse(updated);
}
@Override
@Transactional
public void delete(Long id) {
if (!taskRepository.existsById(id)) {
throw new TaskNotFoundException("Task not found with id: " + id);
}
taskRepository.deleteById(id);
}
}
+24
View File
@@ -0,0 +1,24 @@
spring.application.name=todo
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:tododb}
spring.datasource.username=${DB_USER:postgres}
spring.datasource.password=${DB_PASSWORD:postgres}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=always
management.endpoint.prometheus.enabled=true
management.metrics.export.prometheus.enabled=true
management.metrics.tags.application=${spring.application.name}
management.metrics.distribution.percentiles-histogram.http.server.requests=true