ItemController.java
package com.oliwier.listmebackend.api;
import com.oliwier.listmebackend.api.dto.CreateItemRequest;
import com.oliwier.listmebackend.api.dto.ItemResponse;
import com.oliwier.listmebackend.api.dto.UpdateItemRequest;
import com.oliwier.listmebackend.domain.model.Device;
import com.oliwier.listmebackend.domain.service.ItemService;
import com.oliwier.listmebackend.identity.CurrentDevice;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/lists/{listId}/items")
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ItemController {
private final ItemService itemService;
private final SimpMessagingTemplate messaging;
@GetMapping
public List<ItemResponse> getItems(@PathVariable UUID listId,
@CurrentDevice Device device,
@RequestParam(required = false) String q) {
return itemService.getByList(listId, device, q).stream().map(ItemResponse::from).toList();
}
@GetMapping("/trash")
public List<ItemResponse> getTrash(@PathVariable UUID listId,
@CurrentDevice Device device) {
return itemService.getTrash(listId, device).stream().map(ItemResponse::from).toList();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Transactional
public ItemResponse create(@PathVariable UUID listId,
@CurrentDevice Device device,
@Valid @RequestBody CreateItemRequest req) {
ItemResponse item = ItemResponse.from(itemService.create(listId, device, req));
broadcast(listId, device, "ITEM_CREATE",
Map.of("itemId", item.id(), "name", item.name(), "position", item.position()));
return item;
}
@PutMapping("/{itemId}")
@Transactional
public ItemResponse update(@PathVariable UUID listId,
@PathVariable UUID itemId,
@CurrentDevice Device device,
@Valid @RequestBody UpdateItemRequest req) {
ItemResponse item = ItemResponse.from(itemService.update(listId, itemId, device, req));
broadcast(listId, device, "ITEM_UPDATE",
Map.of("itemId", item.id(), "name", item.name()));
return item;
}
@PatchMapping("/{itemId}/check")
@Transactional
public ItemResponse toggleCheck(@PathVariable UUID listId,
@PathVariable UUID itemId,
@CurrentDevice Device device) {
ItemResponse item = ItemResponse.from(itemService.toggleCheck(listId, itemId, device));
broadcast(listId, device, "ITEM_CHECK",
Map.of("itemId", item.id(), "checked", item.checked()));
return item;
}
/** Soft-delete: moves item to trash. */
@DeleteMapping("/{itemId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
public void delete(@PathVariable UUID listId,
@PathVariable UUID itemId,
@CurrentDevice Device device) {
itemService.delete(listId, itemId, device);
broadcast(listId, device, "ITEM_DELETE", Map.of("itemId", itemId));
}
/** Restore an item from trash. */
@PatchMapping("/{itemId}/restore")
@Transactional
public ItemResponse restore(@PathVariable UUID listId,
@PathVariable UUID itemId,
@CurrentDevice Device device) {
ItemResponse item = ItemResponse.from(itemService.restore(listId, itemId, device));
broadcast(listId, device, "ITEM_CREATE",
Map.of("itemId", item.id(), "name", item.name(), "position", item.position()));
return item;
}
/** Permanently delete a trashed item — irreversible. */
@DeleteMapping("/{itemId}/permanent")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
public void permanentDelete(@PathVariable UUID listId,
@PathVariable UUID itemId,
@CurrentDevice Device device) {
itemService.permanentDelete(listId, itemId, device);
}
private void broadcast(UUID listId, Device device, String opType, Map<String, Object> payload) {
Object event = Map.of(
"id", UUID.randomUUID().toString(),
"listId", listId.toString(),
"deviceId", device.getId().toString(),
"operationType", opType,
"payload", payload,
"vectorClock", Map.of(),
"createdAt", Instant.now().toString()
);
messaging.convertAndSend("/topic/list/" + listId, event);
}
}