| 1 | package edu.ucsb.cs.scaffold.controller; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.annotation.JsonProperty; | |
| 4 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 5 | import com.fasterxml.jackson.core.type.TypeReference; | |
| 6 | import com.fasterxml.jackson.databind.JsonNode; | |
| 7 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 8 | import edu.ucsb.cs.scaffold.model.UserState; | |
| 9 | import edu.ucsb.cs.scaffold.repository.UserStateRepository; | |
| 10 | import io.swagger.v3.oas.annotations.Operation; | |
| 11 | import io.swagger.v3.oas.annotations.Parameter; | |
| 12 | import io.swagger.v3.oas.annotations.tags.Tag; | |
| 13 | import java.util.List; | |
| 14 | import lombok.RequiredArgsConstructor; | |
| 15 | import org.springframework.dao.DataIntegrityViolationException; | |
| 16 | import org.springframework.http.ResponseEntity; | |
| 17 | import org.springframework.web.bind.annotation.*; | |
| 18 | ||
| 19 | @Tag(name = "User State") | |
| 20 | @RestController | |
| 21 | @RequiredArgsConstructor | |
| 22 | public class UserStateController { | |
| 23 | ||
| 24 | private final UserStateRepository userStateRepository; | |
| 25 | private final ObjectMapper objectMapper; | |
| 26 | ||
| 27 | @Operation(summary = "Get saved user state by user ID and course ID") | |
| 28 | @GetMapping("/api/user-state") | |
| 29 | public ResponseEntity<UserStateResponse> getUserState( | |
| 30 | @Parameter(description = "User ID (from users table)") @RequestParam Long userid, | |
| 31 | @Parameter(description = "Course ID (from course table)") @RequestParam Long courseId) { | |
| 32 | // A user with no saved state yet gets empty defaults rather than a 404, so the | |
| 33 | // frontend's useBackend query treats "brand new user" as data, not an error. | |
| 34 |
1
1. getUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::getUserState → KILLED |
return ResponseEntity.ok( |
| 35 | userStateRepository | |
| 36 | .findByUseridAndCourseId(userid, courseId) | |
| 37 | .map( | |
| 38 | state -> | |
| 39 |
1
1. lambda$getUserState$0 : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$0 → KILLED |
new UserStateResponse( |
| 40 | parseStringList(state.getStarredIds()), | |
| 41 | parseJsonNode(state.getDetailCards()), | |
| 42 | parseStringList(state.getMasteredSubconcepts()), | |
| 43 | parseJsonNode(state.getTopLevelPositions()))) | |
| 44 | .orElseGet( | |
| 45 | () -> | |
| 46 |
1
1. lambda$getUserState$1 : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::lambda$getUserState$1 → KILLED |
new UserStateResponse( |
| 47 | List.of(), | |
| 48 | objectMapper.createArrayNode(), | |
| 49 | List.of(), | |
| 50 | objectMapper.createObjectNode()))); | |
| 51 | } | |
| 52 | ||
| 53 | @Operation(summary = "Upsert saved user state by user ID and course ID") | |
| 54 | @PostMapping("/api/user-state") | |
| 55 | public ResponseEntity<Void> upsertUserState(@RequestBody UserStateRequest body) { | |
| 56 |
2
1. upsertUserState : negated conditional → KILLED 2. upsertUserState : negated conditional → KILLED |
if (body.userid() == null || body.courseId() == null) { |
| 57 |
1
1. upsertUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED |
return ResponseEntity.badRequest().build(); |
| 58 | } | |
| 59 | ||
| 60 | try { | |
| 61 |
1
1. upsertUserState : removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED |
upsert(body); |
| 62 | } catch (DataIntegrityViolationException e) { | |
| 63 | // Two near-simultaneous saves for a brand new (userid, courseId) pair (e.g. React | |
| 64 | // StrictMode double-invoking a state updater in dev) can both find no existing row and | |
| 65 | // both attempt an insert; the second violates the unique constraint. Retry once as an | |
| 66 | // update against the row the other request just inserted. | |
| 67 |
1
1. upsertUserState : removed call to edu/ucsb/cs/scaffold/controller/UserStateController::upsert → KILLED |
upsert(body); |
| 68 | } | |
| 69 | ||
| 70 |
1
1. upsertUserState : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::upsertUserState → KILLED |
return ResponseEntity.noContent().build(); |
| 71 | } | |
| 72 | ||
| 73 | private void upsert(UserStateRequest body) { | |
| 74 | UserState state = | |
| 75 | userStateRepository | |
| 76 | .findByUseridAndCourseId(body.userid(), body.courseId()) | |
| 77 | .orElseGet(UserState::new); | |
| 78 |
1
1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setUserid → KILLED |
state.setUserid(body.userid()); |
| 79 |
1
1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setCourseId → KILLED |
state.setCourseId(body.courseId()); |
| 80 |
2
1. upsert : negated conditional → KILLED 2. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setStarredIds → KILLED |
state.setStarredIds(writeJson(body.starredIds() == null ? List.of() : body.starredIds())); |
| 81 |
1
1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setDetailCards → KILLED |
state.setDetailCards( |
| 82 | writeJson( | |
| 83 |
1
1. upsert : negated conditional → KILLED |
body.detailCards() == null ? objectMapper.createArrayNode() : body.detailCards())); |
| 84 |
1
1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setMasteredSubconcepts → KILLED |
state.setMasteredSubconcepts( |
| 85 |
1
1. upsert : negated conditional → KILLED |
writeJson(body.masteredSubconcepts() == null ? List.of() : body.masteredSubconcepts())); |
| 86 |
1
1. upsert : removed call to edu/ucsb/cs/scaffold/model/UserState::setTopLevelPositions → KILLED |
state.setTopLevelPositions( |
| 87 | writeJson( | |
| 88 |
1
1. upsert : negated conditional → KILLED |
body.topLevelPositions() == null |
| 89 | ? objectMapper.createObjectNode() | |
| 90 | : body.topLevelPositions())); | |
| 91 | userStateRepository.save(state); | |
| 92 | } | |
| 93 | ||
| 94 | private List<String> parseStringList(String json) { | |
| 95 | try { | |
| 96 |
1
1. parseStringList : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/controller/UserStateController::parseStringList → KILLED |
return objectMapper.readValue(json, new TypeReference<>() {}); |
| 97 | } catch (JsonProcessingException e) { | |
| 98 | throw new IllegalStateException("Unable to parse stored string list JSON", e); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | private JsonNode parseJsonNode(String json) { | |
| 103 | try { | |
| 104 |
1
1. parseJsonNode : replaced return value with null for edu/ucsb/cs/scaffold/controller/UserStateController::parseJsonNode → KILLED |
return objectMapper.readTree(json); |
| 105 | } catch (JsonProcessingException e) { | |
| 106 | throw new IllegalStateException("Unable to parse stored JSON payload", e); | |
| 107 | } | |
| 108 | } | |
| 109 | ||
| 110 | private String writeJson(Object value) { | |
| 111 | try { | |
| 112 |
1
1. writeJson : replaced return value with "" for edu/ucsb/cs/scaffold/controller/UserStateController::writeJson → KILLED |
return objectMapper.writeValueAsString(value); |
| 113 | } catch (JsonProcessingException e) { | |
| 114 | throw new IllegalArgumentException("Unable to serialize JSON payload", e); | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | public record UserStateRequest( | |
| 119 | Long userid, | |
| 120 | @JsonProperty("courseId") Long courseId, | |
| 121 | @JsonProperty("starred_ids") List<String> starredIds, | |
| 122 | @JsonProperty("detail_cards") JsonNode detailCards, | |
| 123 | @JsonProperty("mastered_subconcepts") List<String> masteredSubconcepts, | |
| 124 | @JsonProperty("top_level_positions") JsonNode topLevelPositions) {} | |
| 125 | ||
| 126 | public record UserStateResponse( | |
| 127 | @JsonProperty("starred_ids") List<String> starredIds, | |
| 128 | @JsonProperty("detail_cards") JsonNode detailCards, | |
| 129 | @JsonProperty("mastered_subconcepts") List<String> masteredSubconcepts, | |
| 130 | @JsonProperty("top_level_positions") JsonNode topLevelPositions) {} | |
| 131 | } | |
Mutations | ||
| 34 |
1.1 |
|
| 39 |
1.1 |
|
| 46 |
1.1 |
|
| 56 |
1.1 2.2 |
|
| 57 |
1.1 |
|
| 61 |
1.1 |
|
| 67 |
1.1 |
|
| 70 |
1.1 |
|
| 78 |
1.1 |
|
| 79 |
1.1 |
|
| 80 |
1.1 2.2 |
|
| 81 |
1.1 |
|
| 83 |
1.1 |
|
| 84 |
1.1 |
|
| 85 |
1.1 |
|
| 86 |
1.1 |
|
| 88 |
1.1 |
|
| 96 |
1.1 |
|
| 104 |
1.1 |
|
| 112 |
1.1 |