Skip to content

Commit 2f9115f

Browse files
authored
Merge pull request #33 from thughari/codex/implement-resource-caching-and-file-upload
Codex/implement resource caching and file upload
2 parents 86850d9 + 99c34eb commit 2f9115f

File tree

7 files changed

+457
-48
lines changed

7 files changed

+457
-48
lines changed

backend/src/main/java/com/thughari/jobtrackerpro/controller/CareerResourceController.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.thughari.jobtrackerpro.dto.CareerResourceDTO;
44
import com.thughari.jobtrackerpro.dto.CareerResourcePageResponse;
55
import com.thughari.jobtrackerpro.dto.CreateCareerResourceRequest;
6+
import com.thughari.jobtrackerpro.dto.UpdateCareerResourceRequest;
67
import com.thughari.jobtrackerpro.service.CareerResourceService;
78
import org.springframework.http.MediaType;
89
import org.springframework.http.ResponseEntity;
@@ -13,13 +14,15 @@
1314
import org.springframework.web.bind.annotation.GetMapping;
1415
import org.springframework.web.bind.annotation.PathVariable;
1516
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.PutMapping;
1618
import org.springframework.web.bind.annotation.RequestBody;
1719
import org.springframework.web.bind.annotation.RequestMapping;
1820
import org.springframework.web.bind.annotation.RequestParam;
1921
import org.springframework.web.bind.annotation.RestController;
2022
import org.springframework.web.multipart.MultipartFile;
2123

2224
import java.util.UUID;
25+
import java.util.List;
2326

2427
@RestController
2528
@RequestMapping("/api/resources")
@@ -34,9 +37,12 @@ public CareerResourceController(CareerResourceService careerResourceService) {
3437
@GetMapping
3538
public ResponseEntity<CareerResourcePageResponse> getResources(
3639
@RequestParam(defaultValue = "0") int page,
37-
@RequestParam(defaultValue = "20") int size
40+
@RequestParam(defaultValue = "20") int size,
41+
@RequestParam(required = false) String query,
42+
@RequestParam(required = false) String category,
43+
@RequestParam(required = false) String type
3844
) {
39-
return ResponseEntity.ok(careerResourceService.getResourcePage(page, size, getAuthenticatedEmailOrNull()));
45+
return ResponseEntity.ok(careerResourceService.getResourcePage(page, size, query, category, type, getAuthenticatedEmailOrNull()));
4046
}
4147

4248
@PostMapping
@@ -45,6 +51,19 @@ public ResponseEntity<CareerResourceDTO> addResource(@RequestBody CreateCareerRe
4551
return ResponseEntity.ok(careerResourceService.createResource(email, request));
4652
}
4753

54+
@GetMapping("/mine")
55+
public ResponseEntity<List<CareerResourceDTO>> getMyResources() {
56+
String email = getAuthenticatedEmail();
57+
return ResponseEntity.ok(careerResourceService.getMyResources(email));
58+
}
59+
60+
@PutMapping("/{id}")
61+
public ResponseEntity<CareerResourceDTO> updateResource(@PathVariable UUID id,
62+
@RequestBody UpdateCareerResourceRequest request) {
63+
String email = getAuthenticatedEmail();
64+
return ResponseEntity.ok(careerResourceService.updateResource(email, id, request));
65+
}
66+
4867
@PostMapping(path = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
4968
public ResponseEntity<CareerResourceDTO> uploadResource(
5069
@RequestParam String title,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.thughari.jobtrackerpro.dto;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class UpdateCareerResourceRequest {
7+
private String title;
8+
private String url;
9+
private String category;
10+
private String description;
11+
}

backend/src/main/java/com/thughari/jobtrackerpro/repo/CareerResourceRepository.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import com.thughari.jobtrackerpro.entity.CareerResource;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
56
import org.springframework.data.domain.Page;
67
import org.springframework.data.domain.Pageable;
78

89
import java.util.List;
910
import java.util.UUID;
1011

11-
public interface CareerResourceRepository extends JpaRepository<CareerResource, UUID> {
12+
public interface CareerResourceRepository extends JpaRepository<CareerResource, UUID>, JpaSpecificationExecutor<CareerResource> {
1213
List<CareerResource> findAllByOrderByCreatedAtDesc();
1314

1415
Page<CareerResource> findAllByOrderByCreatedAtDesc(Pageable pageable);
1516

17+
List<CareerResource> findAllBySubmittedByEmailOrderByCreatedAtDesc(String email);
18+
1619
boolean existsByUrl(String url);
1720
}

backend/src/main/java/com/thughari/jobtrackerpro/service/CareerResourceService.java

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.thughari.jobtrackerpro.dto.CareerResourceDTO;
44
import com.thughari.jobtrackerpro.dto.CareerResourcePageResponse;
55
import com.thughari.jobtrackerpro.dto.CreateCareerResourceRequest;
6+
import com.thughari.jobtrackerpro.dto.UpdateCareerResourceRequest;
67
import com.thughari.jobtrackerpro.entity.CareerResource;
78
import com.thughari.jobtrackerpro.entity.User;
89
import com.thughari.jobtrackerpro.interfaces.StorageService;
@@ -13,15 +14,22 @@
1314
import org.springframework.cache.annotation.Cacheable;
1415
import org.springframework.data.domain.PageRequest;
1516
import org.springframework.data.domain.Sort;
17+
import org.springframework.data.jpa.domain.Specification;
1618
import org.springframework.stereotype.Service;
1719
import org.springframework.transaction.annotation.Transactional;
1820
import org.springframework.web.multipart.MultipartFile;
1921

22+
import jakarta.persistence.criteria.Predicate;
23+
24+
import java.util.ArrayList;
25+
import java.util.Locale;
26+
2027
@Service
2128
@Transactional
2229
public class CareerResourceService {
2330

2431
private static final int MAX_PAGE_SIZE = 50;
32+
private static final int DEMO_MIN_RESOURCE_COUNT = 60;
2533

2634
private final CareerResourceRepository resourceRepository;
2735
private final UserRepository userRepository;
@@ -36,13 +44,21 @@ public CareerResourceService(CareerResourceRepository resourceRepository,
3644
}
3745

3846
@Transactional(readOnly = true)
39-
@Cacheable(value = "resourcePages", key = "{#page, #size, #viewerEmail == null ? 'anon' : #viewerEmail}")
40-
public CareerResourcePageResponse getResourcePage(int page, int size, String viewerEmail) {
47+
@Cacheable(value = "resourcePages", key = "{#page, #size, #query == null ? '' : #query, #category == null ? '' : #category, #type == null ? '' : #type, #viewerEmail == null ? 'anon' : #viewerEmail}")
48+
public CareerResourcePageResponse getResourcePage(int page,
49+
int size,
50+
String query,
51+
String category,
52+
String type,
53+
String viewerEmail) {
4154
int sanitizedPage = Math.max(0, page);
4255
int sanitizedSize = Math.max(1, Math.min(size, MAX_PAGE_SIZE));
56+
String normalizedQuery = normalizeFilter(query);
57+
String normalizedCategory = normalizeFilter(category);
58+
String normalizedType = normalizeType(type);
4359

4460
var pageable = PageRequest.of(sanitizedPage, sanitizedSize, Sort.by(Sort.Direction.DESC, "createdAt"));
45-
var resourcePage = resourceRepository.findAllByOrderByCreatedAtDesc(pageable);
61+
var resourcePage = resourceRepository.findAll(buildResourceFilter(normalizedQuery, normalizedCategory, normalizedType), pageable);
4662

4763
var content = resourcePage.getContent()
4864
.stream()
@@ -59,6 +75,62 @@ public CareerResourcePageResponse getResourcePage(int page, int size, String vie
5975
);
6076
}
6177

78+
private Specification<CareerResource> buildResourceFilter(String query, String category, String type) {
79+
return (root, criteriaQuery, criteriaBuilder) -> {
80+
var predicates = new ArrayList<Predicate>();
81+
82+
if (query != null) {
83+
String likeQuery = "%" + query.toLowerCase(Locale.ROOT) + "%";
84+
predicates.add(criteriaBuilder.or(
85+
criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), likeQuery),
86+
criteriaBuilder.like(criteriaBuilder.lower(root.get("category")), likeQuery),
87+
criteriaBuilder.like(criteriaBuilder.lower(root.get("description")), likeQuery),
88+
criteriaBuilder.like(criteriaBuilder.lower(root.get("submittedByName")), likeQuery)
89+
));
90+
}
91+
92+
if (category != null) {
93+
String likeCategory = "%" + category.toLowerCase(Locale.ROOT) + "%";
94+
predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("category")), likeCategory));
95+
}
96+
97+
if (type != null) {
98+
predicates.add(criteriaBuilder.equal(criteriaBuilder.upper(root.get("resourceType")), type));
99+
}
100+
101+
return predicates.isEmpty()
102+
? criteriaBuilder.conjunction()
103+
: criteriaBuilder.and(predicates.toArray(new Predicate[0]));
104+
};
105+
}
106+
107+
private String normalizeFilter(String value) {
108+
if (value == null) {
109+
return null;
110+
}
111+
112+
String trimmed = value.trim();
113+
if (trimmed.isEmpty() || "all".equalsIgnoreCase(trimmed)) {
114+
return null;
115+
}
116+
117+
return trimmed;
118+
}
119+
120+
private String normalizeType(String value) {
121+
String normalized = normalizeFilter(value);
122+
if (normalized == null) {
123+
return null;
124+
}
125+
126+
String upper = normalized.toUpperCase(Locale.ROOT);
127+
if (!upper.equals("LINK") && !upper.equals("FILE")) {
128+
return null;
129+
}
130+
131+
return upper;
132+
}
133+
62134
@CacheEvict(value = "resourcePages", allEntries = true)
63135
public CareerResourceDTO createResource(String email, CreateCareerResourceRequest request) {
64136
String normalizedUrl = normalizeUrl(request.getUrl());
@@ -120,17 +192,79 @@ public void deleteResource(String email, java.util.UUID resourceId) {
120192
resourceRepository.delete(resource);
121193
}
122194

123-
@PostConstruct
124-
public void seedStarterResources() {
125-
if (resourceRepository.count() > 0) {
126-
return;
195+
@Transactional(readOnly = true)
196+
public java.util.List<CareerResourceDTO> getMyResources(String email) {
197+
return resourceRepository.findAllBySubmittedByEmailOrderByCreatedAtDesc(email)
198+
.stream()
199+
.map(resource -> toDTO(resource, email))
200+
.toList();
201+
}
202+
203+
@CacheEvict(value = "resourcePages", allEntries = true)
204+
public CareerResourceDTO updateResource(String email, java.util.UUID resourceId, UpdateCareerResourceRequest request) {
205+
CareerResource resource = resourceRepository.findById(resourceId)
206+
.orElseThrow(() -> new IllegalArgumentException("Resource not found"));
207+
208+
if (!resource.getSubmittedByEmail().equalsIgnoreCase(email)) {
209+
throw new IllegalArgumentException("You can only edit resources you added");
210+
}
211+
212+
validateCommonFields(request.getTitle(), request.getCategory());
213+
214+
resource.setTitle(request.getTitle().trim());
215+
resource.setCategory(request.getCategory().trim());
216+
resource.setDescription(request.getDescription() == null ? null : request.getDescription().trim());
217+
218+
if ("LINK".equalsIgnoreCase(resource.getResourceType())) {
219+
String normalizedUrl = normalizeUrl(request.getUrl());
220+
if (normalizedUrl == null || normalizedUrl.isBlank()) {
221+
throw new IllegalArgumentException("Valid URL is required for link resources");
222+
}
223+
resource.setUrl(normalizedUrl);
127224
}
128225

226+
return toDTO(resourceRepository.save(resource), email);
227+
}
228+
229+
@PostConstruct
230+
public void seedStarterResources() {
129231
addSeedResource("Career Preparation Notes", "https://docs.google.com/document/d/1-25JrPUai6P7pjKk1g7mELpI0YUINS_NpjUk7lsMfyg/edit?tab=t.0#heading=h.kf8l3f8jftc2", "Guides & Study Docs");
130232
addSeedResource("Main Career Resources Folder", "https://drive.google.com/drive/folders/1ISp9GBv7ih1blEQOPYplD_idG1j0ibGq?usp=sharing", "Drive Folders & File Packs");
131233
addSeedResource("DSA Folder", "https://drive.google.com/drive/folders/1ei52Zc_cQe0rJK404M56BmEisUjnF6kN?usp=drive_link", "Drive Folders & File Packs");
132234
addSeedResource("21 Days React Study Plan", "https://thecodedose.notion.site/21-Days-React-Study-Plan-1988ff023cae48459bae8cb20cb75a67", "Structured Learning");
133235
addSeedResource("Opportunity Tracker Sheet", "https://docs.google.com/spreadsheets/d/1KBFiqJTaFY1164XtglKvn2vAofScCfGlkY-n54D2d14/edit?gid=584790886#gid=584790886", "Trackers & Opportunity Sheets");
236+
237+
seedDemoVolumeResources();
238+
}
239+
240+
private void seedDemoVolumeResources() {
241+
long currentCount = resourceRepository.count();
242+
if (currentCount >= DEMO_MIN_RESOURCE_COUNT) {
243+
return;
244+
}
245+
246+
String[][] templates = {
247+
{"React Interview Drill", "https://example.com/resources/react-interview-drill", "Interview Prep"},
248+
{"DSA Patterns Workbook", "https://example.com/resources/dsa-patterns-workbook", "DSA"},
249+
{"System Design Primer", "https://example.com/resources/system-design-primer", "System Design"},
250+
{"Resume Bullet Bank", "https://example.com/resources/resume-bullet-bank", "Resume"},
251+
{"Backend Fundamentals Roadmap", "https://example.com/resources/backend-fundamentals-roadmap", "Roadmaps"},
252+
{"Frontend Fundamentals Roadmap", "https://example.com/resources/frontend-fundamentals-roadmap", "Roadmaps"},
253+
{"Mock Interview Checklist", "https://example.com/resources/mock-interview-checklist", "Mock Interviews"},
254+
{"Job Board Tracker", "https://example.com/resources/job-board-tracker", "Job Boards"},
255+
{"Behavioral Question Matrix", "https://example.com/resources/behavioral-question-matrix", "Interview Prep"},
256+
{"Portfolio Project Ideas", "https://example.com/resources/portfolio-project-ideas", "Portfolio"}
257+
};
258+
259+
long resourcesToAdd = DEMO_MIN_RESOURCE_COUNT - currentCount;
260+
for (int i = 0; i < resourcesToAdd; i++) {
261+
String[] template = templates[i % templates.length];
262+
String title = template[0] + " #" + (i + 1);
263+
String url = template[1] + "?v=" + (i + 1);
264+
String category = template[2];
265+
266+
addSeedResource(title, url, category);
267+
}
134268
}
135269

136270
private void addSeedResource(String title, String url, String category) {

0 commit comments

Comments
 (0)