Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
06d98c8
feat: implement Phase 6 – update, delete, and relationship management…
jfontdev Mar 28, 2026
6472203
feat: add JPA properties for Hibernate default batch fetch size, this…
jfontdev Mar 28, 2026
0cad66a
feat: add validation annotations to TrackPatchRequestDTO and update p…
jfontdev Mar 28, 2026
1ccde15
feat: improve TrackServiceImpl by enhancing tag management and ensuri…
jfontdev Mar 28, 2026
12efde0
fix: use imported Comparator in TrackServiceImpl mapToResponseDTO
Copilot Mar 28, 2026
b5b46d5
Update src/main/java/com/jfontdev/trackstack/controller/TagController…
jfontdev Mar 28, 2026
354b69f
Update src/main/java/com/jfontdev/trackstack/controller/PlaylistContr…
jfontdev Mar 28, 2026
74746ba
Update src/main/java/com/jfontdev/trackstack/service/impl/TrackServic…
jfontdev Mar 28, 2026
40ee495
fix: add @Size validations to TagPatchRequestDTO and PlaylistPatchReq…
Copilot Mar 28, 2026
3875140
Initial plan
Copilot Mar 28, 2026
006667c
Merge pull request #16 from jfontdev/copilot/sub-pr-15
jfontdev Mar 28, 2026
7f69b40
feat: enhance cache eviction in deleteTrack method to include playlists
jfontdev Mar 28, 2026
6e93dae
feat: update cache eviction strategy in TagServiceImpl and TrackServi…
jfontdev Mar 28, 2026
8ea21ae
feat: add validation constraints to TrackRequestDTO and TrackUpdateRe…
jfontdev Mar 28, 2026
b85d903
feat: add validation constraints to Playlist and Tag DTOs for improve…
jfontdev Mar 28, 2026
7355c04
feat: add validation tests for Playlist and Tag APIs to ensure data i…
jfontdev Mar 28, 2026
5dae0b0
feat: return unmodifiable views of tracks and tags in Playlist and Tr…
jfontdev Mar 28, 2026
687151f
Update src/main/java/com/jfontdev/trackstack/exception/GlobalExceptio…
jfontdev Mar 28, 2026
fdcd023
Update src/main/java/com/jfontdev/trackstack/dto/track/TrackUpdateReq…
jfontdev Mar 28, 2026
d8d3660
Update src/main/java/com/jfontdev/trackstack/dto/track/TrackRequestDT…
jfontdev Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
Comment thread
jfontdev marked this conversation as resolved.
22 changes: 15 additions & 7 deletions docs/project-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,23 @@ Status: ✅ Completed

---

## 🔜 Phase 06 – Update & Delete Operations
## Phase 06 – Update & Delete Operations

**Goal:** Complete CRUD cycle properly.

- PUT endpoints
- DELETE endpoints
- Cache invalidation on writes
- Proper transaction boundaries
- Validation rules
- PUT endpoints (full update with validation)
- PATCH endpoints (partial update, nullable fields)
- DELETE endpoints (with 204 No Content)
- Relationship management endpoints (add/remove tags from tracks, tracks from playlists)
- Entity `update()` methods for controlled mutation
- V5 Flyway migration: `ON DELETE CASCADE` on join table foreign keys
- Enriched response DTOs (tracks include tags, playlists include tracks)
- `DataIntegrityViolationException` → 409 Conflict handler
- `@Transactional` + `@CacheEvict(allEntries=true)` on all write operations
- Update/Patch request DTOs per entity
- Integration tests for all new endpoints

Status: ✅ Completed

---

Expand Down Expand Up @@ -249,4 +257,4 @@ The project will be considered “enterprise-ready” when:

# 📌 Current Phase

👉 Phase 06Update & Delete Operations
👉 Phase 07Pagination & Filtering
Empty file modified mvnw
100644 → 100755
Empty file.
178 changes: 140 additions & 38 deletions src/main/java/com/jfontdev/trackstack/controller/PlaylistController.java
Original file line number Diff line number Diff line change
@@ -1,38 +1,140 @@
package com.jfontdev.trackstack.controller;

import com.jfontdev.trackstack.dto.playlist.PlaylistRequestDTO;
import com.jfontdev.trackstack.dto.playlist.PlaylistResponseDTO;
import com.jfontdev.trackstack.service.PlaylistService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/playlists")
public class PlaylistController {

private final PlaylistService playlistService;

public PlaylistController(PlaylistService playlistService) {
this.playlistService = playlistService;
}

@PostMapping
public ResponseEntity<PlaylistResponseDTO> create(@Valid @RequestBody PlaylistRequestDTO dto) {
PlaylistResponseDTO response = playlistService.createPlaylist(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@GetMapping("/{id}")
public PlaylistResponseDTO getById(@PathVariable Long id) {
return playlistService.getPlaylistById(id);
}

@GetMapping
public List<PlaylistResponseDTO> getAll() {
return playlistService.getAllPlaylists();
}
}
package com.jfontdev.trackstack.controller;

import com.jfontdev.trackstack.dto.playlist.PlaylistPatchRequestDTO;
import com.jfontdev.trackstack.dto.playlist.PlaylistRequestDTO;
import com.jfontdev.trackstack.dto.playlist.PlaylistResponseDTO;
import com.jfontdev.trackstack.dto.playlist.PlaylistUpdateRequestDTO;
import com.jfontdev.trackstack.service.PlaylistService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* REST controller for managing playlists.
* <p>
* Provides endpoints for full CRUD operations on playlists, as well as
* track relationship management. This controller delegates all business
* logic to the {@link PlaylistService} and only handles HTTP concerns
* (request binding, status codes, response formatting).
*/
@RestController
@RequestMapping("/api/playlists")
public class PlaylistController {

private final PlaylistService playlistService;

/**
* Constructs a new {@code PlaylistController} with the required service.
*
* @param playlistService the service handling playlist business logic
*/
public PlaylistController(PlaylistService playlistService) {
this.playlistService = playlistService;
}

/**
* Creates a new playlist.
*
* @param dto the validated request body containing playlist details
* @return 201 Created with the newly created playlist
*/
@PostMapping
public ResponseEntity<PlaylistResponseDTO> create(@Valid @RequestBody PlaylistRequestDTO dto) {
PlaylistResponseDTO response = playlistService.createPlaylist(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/**
* Retrieves a playlist by its ID.
*
* @param id the playlist's unique identifier
* @return 200 OK with the playlist details (including tracks), or 404 if not found
*/
@GetMapping("/{id}")
public PlaylistResponseDTO getById(@PathVariable Long id) {
return playlistService.getPlaylistById(id);
}

/**
* Retrieves all playlists.
*
* @return 200 OK with a list of all playlists (each including their tracks)
*/
@GetMapping
public List<PlaylistResponseDTO> getAll() {
return playlistService.getAllPlaylists();
}

/**
* Fully updates an existing playlist (PUT semantics).
* <p>
* All fields in the request body replace the existing values.
*
* @param id the playlist's unique identifier
* @param dto the validated request body containing the new playlist details
* @return 200 OK with the updated playlist, or 404 if not found
*/
@PutMapping("/{id}")
public PlaylistResponseDTO update(@PathVariable Long id, @Valid @RequestBody PlaylistUpdateRequestDTO dto) {
return playlistService.updatePlaylist(id, dto);
}

/**
* Partially updates an existing playlist (PATCH semantics).
* <p>
* Only non-null fields in the request body are applied to the existing playlist.
*
* @param id the playlist's unique identifier
* @param dto the validated request body containing the fields to update
* @return 200 OK with the updated playlist, or 404 if not found
*/
@PatchMapping("/{id}")
public PlaylistResponseDTO patch(@PathVariable Long id, @Valid @RequestBody PlaylistPatchRequestDTO dto) {
return playlistService.patchPlaylist(id, dto);
}

/**
* Deletes a playlist by its ID.
* <p>
* The tracks themselves are not deleted -- only the playlist and its
* track associations are removed (handled by ON DELETE CASCADE at the
* database level).
*
* @param id the playlist's unique identifier
* @return 204 No Content on success, or 404 if not found
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
playlistService.deletePlaylist(id);
return ResponseEntity.noContent().build();
}

/**
* Adds a track to a playlist.
*
* @param id the playlist's unique identifier
* @param trackId the track's unique identifier
* @return 200 OK with the updated playlist (including the new track), or 404 if
* either the playlist or track is not found
*/
@PutMapping("/{id}/tracks/{trackId}")
public PlaylistResponseDTO addTrack(@PathVariable Long id, @PathVariable Long trackId) {
return playlistService.addTrackToPlaylist(id, trackId);
}

/**
* Removes a track from a playlist.
*
* @param id the playlist's unique identifier
* @param trackId the track's unique identifier
* @return 200 OK with the updated playlist (without the removed track), or 404 if
* either the playlist or track is not found
*/
@DeleteMapping("/{id}/tracks/{trackId}")
public PlaylistResponseDTO removeTrack(@PathVariable Long id, @PathVariable Long trackId) {
return playlistService.removeTrackFromPlaylist(id, trackId);
}
}
152 changes: 113 additions & 39 deletions src/main/java/com/jfontdev/trackstack/controller/TagController.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,113 @@
package com.jfontdev.trackstack.controller;


import com.jfontdev.trackstack.dto.tag.TagRequestDTO;
import com.jfontdev.trackstack.dto.tag.TagResponseDTO;
import com.jfontdev.trackstack.service.TagService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/tags")
public class TagController {

private final TagService tagService;

public TagController(TagService tagService) {
this.tagService = tagService;
}

@PostMapping
public ResponseEntity<TagResponseDTO> create(@Valid @RequestBody TagRequestDTO dto) {
TagResponseDTO response = tagService.createTag(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@GetMapping("/{id}")
public TagResponseDTO getById(@PathVariable Long id) {
return tagService.getTagById(id);
}

@GetMapping
public List<TagResponseDTO> getAll() {
return tagService.getAllTags();
}
}
package com.jfontdev.trackstack.controller;

import com.jfontdev.trackstack.dto.tag.TagPatchRequestDTO;
import com.jfontdev.trackstack.dto.tag.TagRequestDTO;
import com.jfontdev.trackstack.dto.tag.TagResponseDTO;
import com.jfontdev.trackstack.dto.tag.TagUpdateRequestDTO;
import com.jfontdev.trackstack.service.TagService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* REST controller for managing tags.
* <p>
* Provides endpoints for full CRUD operations on tags. This controller
* delegates all business logic to the {@link TagService} and only handles
* HTTP concerns (request binding, status codes, response formatting).
*/
@RestController
@RequestMapping("/api/tags")
public class TagController {

private final TagService tagService;

/**
* Constructs a new {@code TagController} with the required service.
*
* @param tagService the service handling tag business logic
*/
public TagController(TagService tagService) {
this.tagService = tagService;
}

/**
* Creates a new tag.
*
* @param dto the validated request body containing tag details
* @return 201 Created with the newly created tag
*/
@PostMapping
public ResponseEntity<TagResponseDTO> create(@Valid @RequestBody TagRequestDTO dto) {
TagResponseDTO response = tagService.createTag(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

/**
* Retrieves a tag by its ID.
*
* @param id the tag's unique identifier
* @return 200 OK with the tag details, or 404 if not found
*/
@GetMapping("/{id}")
public TagResponseDTO getById(@PathVariable Long id) {
return tagService.getTagById(id);
}

/**
* Retrieves all tags.
*
* @return 200 OK with a list of all tags
*/
@GetMapping
public List<TagResponseDTO> getAll() {
return tagService.getAllTags();
}

/**
* Fully updates an existing tag (PUT semantics).
* <p>
* The tag name in the request body replaces the existing value.
* The new name must remain unique.
*
* @param id the tag's unique identifier
* @param dto the validated request body containing the new tag details
* @return 200 OK with the updated tag, or 404 if not found
*/
@PutMapping("/{id}")
public TagResponseDTO update(@PathVariable Long id, @Valid @RequestBody TagUpdateRequestDTO dto) {
return tagService.updateTag(id, dto);
}

/**
* Partially updates an existing tag (PATCH semantics).
* <p>
* Only non-null fields in the request body are applied to the existing tag.
*
* @param id the tag's unique identifier
* @param dto the validated request body containing the fields to update
* @return 200 OK with the updated tag, or 404 if not found
*/
@PatchMapping("/{id}")
public TagResponseDTO patch(@PathVariable Long id, @Valid @RequestBody TagPatchRequestDTO dto) {
return tagService.patchTag(id, dto);
}

/**
* Deletes a tag by its ID.
* <p>
* Also removes all track associations for this tag (handled by
* ON DELETE CASCADE at the database level).
*
* @param id the tag's unique identifier
* @return 204 No Content on success, or 404 if not found
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
tagService.deleteTag(id);
return ResponseEntity.noContent().build();
}
}
Loading