Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
205a080
add migration file for task_skills table
sunilk429 Mar 21, 2025
0a0d814
add TaskSkillId as composite key and TaskSkill entity
sunilk429 Mar 21, 2025
ac4a653
add TaskSkillRepository to find skills by taskId
sunilk429 Mar 21, 2025
819284e
add custom exception for duplicate task-skill association
sunilk429 Mar 21, 2025
0f8f193
add TaskSkillService to handle services related to task and skill
sunilk429 Mar 21, 2025
fe7ead3
add controller TaskSkillApi
sunilk429 Mar 21, 2025
79c9765
add @NotNull validation and serialVersionUID to TaskSkillID model
sunilk429 Mar 24, 2025
e2c5248
fix: remove duplicate skill IDs sent through request body
sunilk429 Mar 27, 2025
587069a
add relationship mapping in TaskSkill and ensure Skill is fetched bef…
sunilk429 Mar 27, 2025
389c38b
create TaskSkill request and creation view models
sunilk429 Mar 27, 2025
f2819d2
use dedicated TaskSkill request and creation view-models for modularity
sunilk429 Mar 27, 2025
be90dd4
add validation for taskId and move exception handler from TaskSkillAp…
sunilk429 Mar 27, 2025
7b56aba
add detailed JavaDoc to TaskSkillService#createTaskSkills
sunilk429 Mar 27, 2025
4b55066
optimize TaskSkillService by batch retrieving Skill entities for crea…
sunilk429 Mar 28, 2025
6f0227a
optimize TaskSkillService by batch validating and saving TaskSkill as…
sunilk429 Mar 28, 2025
9c30802
Merge branch 'develop' into feat/task-skill-association
iamitprakash Mar 28, 2025
9c76736
update status code 200 to 201 for resource creation
sunilk429 Mar 29, 2025
6f6086d
Merge branch 'Real-Dev-Squad:develop' into feat/task-skill-association
sunilk429 Apr 9, 2025
92df1ea
optimize task-skill creation with batch checks and map-based lookups
sunilk429 Apr 9, 2025
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
41 changes: 41 additions & 0 deletions skill-tree/src/main/java/com/RDS/skilltree/apis/TaskSkillApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.RDS.skilltree.apis;

import com.RDS.skilltree.annotations.AuthorizedRoles;
import com.RDS.skilltree.enums.UserRoleEnum;
import com.RDS.skilltree.models.JwtUser;
import com.RDS.skilltree.services.TaskSkillService;
import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel;
import com.RDS.skilltree.viewmodels.TaskSkillRequestViewModel;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("v1/tasks")
@Validated
public class TaskSkillApi {

private final TaskSkillService taskSkillService;

@AuthorizedRoles({UserRoleEnum.SUPERUSER})
@PostMapping("/{taskId}/skills")
public ResponseEntity<CreateTaskSkillViewModel> createTaskSkills(
@PathVariable @Pattern(regexp = "^[A-Za-z0-9]+$", message = "Task ID must be valid")
String taskId,
@Valid @RequestBody TaskSkillRequestViewModel request) {
JwtUser currentUser =
(JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String createdBy = currentUser.getRdsUserId();
CreateTaskSkillViewModel response =
taskSkillService.createTaskSkills(taskId, request.getSkillIds(), createdBy);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ public ResponseEntity<?> handleSkillNotFoundException(
return new ResponseEntity<>(new GenericResponse<>(null, ex.getMessage()), HttpStatus.NOT_FOUND);
}

@ExceptionHandler(TaskSkillAssociationAlreadyExistsException.class)
public ResponseEntity<GenericResponse<Object>> handleTaskSkillAssociationAlreadyExistsException(
TaskSkillAssociationAlreadyExistsException ex) {
log.error("Duplicate task-skill association: {}", ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new GenericResponse<>(null, ex.getMessage()));
}

@ExceptionHandler(EndorsementNotFoundException.class)
public ResponseEntity<?> handleEndorsementNotException(
EndorsementNotFoundException ex, WebRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.RDS.skilltree.exceptions;

public class TaskSkillAssociationAlreadyExistsException extends RuntimeException {
public TaskSkillAssociationAlreadyExistsException(String message, Throwable cause) {
super(message, cause);
}

public TaskSkillAssociationAlreadyExistsException(String message) {
super(message);
}
}
47 changes: 47 additions & 0 deletions skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkill.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.RDS.skilltree.models;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import lombok.*;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;

@Entity
@Table(name = "task_skills")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TaskSkill {

@EmbeddedId private TaskSkillId id;

@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

@Column(name = "is_deleted", nullable = false)
@Builder.Default
private boolean isDeleted = false;

@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;

@CreatedBy
@Column(name = "created_by", nullable = false, updatable = false)
private String createdBy;

@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;

// Relationship to Skill entity for richer detail access.
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("skillId")
@JoinColumn(name = "skill_id", insertable = false, updatable = false)
private Skill skill;
}
24 changes: 24 additions & 0 deletions skill-tree/src/main/java/com/RDS/skilltree/models/TaskSkillId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.RDS.skilltree.models;

import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;

@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode
public class TaskSkillId implements Serializable {
@Serial private static final long serialVersionUID = 1L;

@NotNull(message = "Task ID cannot be null")
private String taskId;

@NotNull(message = "Skill ID cannot be null")
private Integer skillId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.RDS.skilltree.repositories;

import com.RDS.skilltree.models.TaskSkill;
import com.RDS.skilltree.models.TaskSkillId;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface TaskSkillRepository extends JpaRepository<TaskSkill, TaskSkillId> {
/**
* Find skill IDs that already have associations with the given task ID
*/
@Query(
"SELECT ts.id.skillId FROM TaskSkill ts WHERE ts.id.taskId = :taskId AND ts.id.skillId IN :skillIds")
Set<Integer> findSkillIdsByTaskIdAndSkillIdIn(
@Param("taskId") String taskId, @Param("skillIds") Set<Integer> skillIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.RDS.skilltree.services;

import com.RDS.skilltree.exceptions.SkillNotFoundException;
import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException;
import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel;
import java.util.List;

public interface TaskSkillService {
/**
* Creates associations between a task and multiple skills.
*
* @param taskId The unique identifier of the task.
* @param skillIds List of skill identifiers to associate with the task.
* @param createdBy The identifier of the user creating these associations.
* @return A response view model indicating success.
* @throws TaskSkillAssociationAlreadyExistsException if an association already exists.
* @throws SkillNotFoundException if any skill does not exist.
*/
CreateTaskSkillViewModel createTaskSkills(
String taskId, List<Integer> skillIds, String createdBy);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.RDS.skilltree.services;

import com.RDS.skilltree.exceptions.SkillNotFoundException;
import com.RDS.skilltree.exceptions.TaskSkillAssociationAlreadyExistsException;
import com.RDS.skilltree.models.Skill;
import com.RDS.skilltree.models.TaskSkill;
import com.RDS.skilltree.models.TaskSkillId;
import com.RDS.skilltree.repositories.SkillRepository;
import com.RDS.skilltree.repositories.TaskSkillRepository;
import com.RDS.skilltree.viewmodels.CreateTaskSkillViewModel;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TaskSkillServiceImplementation implements TaskSkillService {

private final TaskSkillRepository taskSkillRepository;
private final SkillRepository skillRepository;

public TaskSkillServiceImplementation(
TaskSkillRepository taskSkillRepository, SkillRepository skillRepository) {
this.taskSkillRepository = taskSkillRepository;
this.skillRepository = skillRepository;
}

@Override
@Transactional
public CreateTaskSkillViewModel createTaskSkills(
String taskId, List<Integer> skillIds, String createdBy) {
// Remove duplicate skill IDs
Set<Integer> uniqueSkillIds = new HashSet<>(skillIds);

// FIRST: Check if any TaskSkill associations already exist (batch check)
Set<Integer> existingSkillIds =
taskSkillRepository.findSkillIdsByTaskIdAndSkillIdIn(taskId, uniqueSkillIds);
if (!existingSkillIds.isEmpty()) {
throw new TaskSkillAssociationAlreadyExistsException(
"Task-Skill associations already exist for task "
+ taskId
+ " and skills: "
+ existingSkillIds);
}

// SECOND: Load all skills at once and verify they exist
List<Skill> skills = skillRepository.findAllById(uniqueSkillIds);

// Check for missing skills
if (skills.size() != uniqueSkillIds.size()) {
Set<Integer> foundSkillIds = skills.stream().map(Skill::getId).collect(Collectors.toSet());
Set<Integer> missingIds = new HashSet<>(uniqueSkillIds);
missingIds.removeAll(foundSkillIds);
throw new SkillNotFoundException("Skill not found for skillId(s): " + missingIds);
}

// Create a map for quick lookups
Map<Integer, Skill> skillsMap = skills.stream().collect(Collectors.toMap(Skill::getId, s -> s));

// Create and save TaskSkill entities
LocalDateTime now = LocalDateTime.now();
List<TaskSkill> taskSkillsToSave =
uniqueSkillIds.stream()
.map(
skillId ->
TaskSkill.builder()
.id(new TaskSkillId(taskId, skillId))
.skill(skillsMap.get(skillId)) // Set the actual Skill entity reference
.createdAt(now)
.createdBy(createdBy)
.build())
.collect(Collectors.toList());

taskSkillRepository.saveAll(taskSkillsToSave);

return new CreateTaskSkillViewModel("Skills are linked to task successfully!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.RDS.skilltree.viewmodels;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreateTaskSkillViewModel {
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.RDS.skilltree.viewmodels;

import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TaskSkillRequestViewModel {
@NotEmpty(message = "Skill IDs list cannot be empty")
private List<Integer> skillIds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE `task_skills` (
`task_id` varchar(255) NOT NULL,
`skill_id` int NOT NULL,
`created_at` datetime(6) NOT NULL,
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`updated_at` datetime(6) DEFAULT NULL,
`created_by` varchar(255) NOT NULL,
`updated_by` varchar(255) DEFAULT NULL,
PRIMARY KEY (`task_id`, `skill_id`),
CONSTRAINT `fk_task_skills_skill_id` FOREIGN KEY (`skill_id`) REFERENCES `skills` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;