Skip to content

Commit

Permalink
Merge pull request #49 from commonground-project/feat/add-timeline-co…
Browse files Browse the repository at this point in the history
…ntroller

Add timeline endpoint
  • Loading branch information
Kyle9410-Chen authored Jan 12, 2025
2 parents 9c81a51 + a74dc8e commit b5bf1fe
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package tw.commonground.backend.service.timeline;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import tw.commonground.backend.service.timeline.dto.*;
import tw.commonground.backend.service.timeline.entity.NodeEntity;

import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api")
public class TimelineController {

private final TimelineService timelineService;

public TimelineController(TimelineService timelineService) {
this.timelineService = timelineService;
}

@GetMapping("/issue/{id}/timeline")
public ResponseEntity<TimelineResponse> getTimeline(@PathVariable UUID id) {
List<NodeEntity> nodes = timelineService.getNodes(id);
TimelineResponse response = TimelineMapper.toResponse(nodes);
return ResponseEntity.ok(response);
}

@PostMapping("/issue/{id}/timeline")
public ResponseEntity<NodeResponse> createNode(@PathVariable UUID id, @Valid @RequestBody NodeRequest request) {
NodeEntity node = timelineService.createNode(id, request);
NodeResponse response = NodeMapper.toResponse(node);
return ResponseEntity.ok(response);
}

@GetMapping("/timeline/node/{nodeId}")
public ResponseEntity<NodeResponse> getNode(@PathVariable UUID nodeId) {
NodeEntity node = timelineService.getNode(nodeId);
NodeResponse response = NodeMapper.toResponse(node);
return ResponseEntity.ok(response);
}

@PutMapping("/timeline/node/{nodeId}")
public ResponseEntity<NodeResponse> updateNode(@PathVariable UUID nodeId, @Valid @RequestBody NodeRequest request) {
NodeEntity node = timelineService.updateNode(nodeId, request);
NodeResponse response = NodeMapper.toResponse(node);
return ResponseEntity.ok(response);
}

@DeleteMapping("/timeline/node/{nodeId}")
public ResponseEntity<Void> deleteNode(@PathVariable UUID nodeId) {
timelineService.deleteNode(nodeId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package tw.commonground.backend.service.timeline;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Service;
import tw.commonground.backend.exception.EntityNotFoundException;
import tw.commonground.backend.exception.ValidationException;
import tw.commonground.backend.service.issue.IssueService;
import tw.commonground.backend.service.issue.entity.IssueEntity;
import tw.commonground.backend.service.timeline.dto.NodeRequest;
import tw.commonground.backend.service.timeline.entity.NodeEntity;
import tw.commonground.backend.service.timeline.entity.NodeRepository;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.UUID;

@Service
public class TimelineService {

private final IssueService issueService;

private final NodeRepository nodeRepository;

@PersistenceContext
private EntityManager entityManager;

public TimelineService(IssueService issueService, NodeRepository nodeRepository) {
this.issueService = issueService;
this.nodeRepository = nodeRepository;
}

public List<NodeEntity> getNodes(UUID issueId) {
issueService.throwIfIssueNotExist(issueId);
return nodeRepository.findAllByIssueId(issueId);
}

public NodeEntity getNode(UUID nodeId) {
return nodeRepository.findById(nodeId)
.orElseThrow(() -> new EntityNotFoundException("Node", "id", nodeId.toString()));
}

public NodeEntity createNode(UUID issueId, NodeRequest request) {
issueService.throwIfIssueNotExist(issueId);

NodeEntity node = new NodeEntity();
node.setTitle(request.getTitle());
node.setDescription(request.getDescription());

try {
node.setDate(LocalDate.parse(request.getDate()));
} catch (DateTimeParseException e) {
throw new ValidationException("date", "Invalid date format");
}

IssueEntity issue = entityManager.getReference(IssueEntity.class, issueId);
node.setIssue(issue);

return nodeRepository.save(node);
}

public NodeEntity updateNode(UUID nodeId, NodeRequest request) {
NodeEntity node = nodeRepository.findById(nodeId)
.orElseThrow(() -> new EntityNotFoundException("Node", "id", nodeId.toString()));

node.setTitle(request.getTitle());
node.setDescription(request.getDescription());

try {
node.setDate(LocalDate.parse(request.getDate()));
} catch (DateTimeParseException e) {
throw new ValidationException("date", "Invalid date format");
}

return nodeRepository.save(node);
}

public void deleteNode(UUID nodeId) {
nodeRepository.deleteById(nodeId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tw.commonground.backend.service.timeline.dto;

import tw.commonground.backend.service.timeline.entity.NodeEntity;

import java.time.format.DateTimeFormatter;

public final class NodeMapper {

private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

private NodeMapper() {
// hide the constructor
}

public static NodeResponse toResponse(NodeEntity entity) {
return NodeResponse.builder()
.id(entity.getId().toString())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.title(entity.getTitle())
.description(entity.getDescription())
.date(entity.getDate().format(FORMATTER))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tw.commonground.backend.service.timeline.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
public class NodeRequest {

@NotBlank(message = "Title is required")
private String title;

private String description;

@NotBlank(message = "Date is required")
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$", message = "Date must be in the format yyyy-MM-dd")
private String date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tw.commonground.backend.service.timeline.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

@Getter
@Setter
@Builder
public class NodeResponse {

private String id;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

private String title;

private String description;

private String date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tw.commonground.backend.service.timeline.dto;

import tw.commonground.backend.service.timeline.entity.NodeEntity;

import java.util.List;
import java.util.stream.Collectors;

public final class TimelineMapper {
private TimelineMapper() {
// hide the constructor
}

public static TimelineResponse toResponse(List<NodeEntity> nodes) {
List<NodeResponse> content = nodes.stream()
.map(NodeMapper::toResponse)
.collect(Collectors.toList());
return new TimelineResponse(content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tw.commonground.backend.service.timeline.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
public class TimelineResponse {
private List<NodeResponse> content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package tw.commonground.backend.service.timeline.dto;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tw.commonground.backend.service.timeline.entity;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import tw.commonground.backend.service.issue.entity.IssueEntity;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class NodeEntity {
@Id
@GeneratedValue
private UUID id;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

private String title;

private String description;

private LocalDate date;

@ManyToOne
private IssueEntity issue;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package tw.commonground.backend.service.timeline.entity;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.UUID;

public interface NodeRepository extends JpaRepository<NodeEntity, UUID> {
List<NodeEntity> findAllByIssueId(UUID issueId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package tw.commonground.backend.service.timeline.entity;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package tw.commonground.backend.service.timeline;

0 comments on commit b5bf1fe

Please sign in to comment.