Add pool repo (gap-based position ordering, cursor pagination, add/remove/reorder files), service, handler, and wire all /pools endpoints including /pools/:id/files, /pools/:id/files/remove, and /pools/:id/files/reorder. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
356 lines
8.4 KiB
Go
356 lines
8.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"tanabata/backend/internal/domain"
|
|
"tanabata/backend/internal/port"
|
|
"tanabata/backend/internal/service"
|
|
)
|
|
|
|
// PoolHandler handles all /pools endpoints.
|
|
type PoolHandler struct {
|
|
poolSvc *service.PoolService
|
|
}
|
|
|
|
// NewPoolHandler creates a PoolHandler.
|
|
func NewPoolHandler(poolSvc *service.PoolService) *PoolHandler {
|
|
return &PoolHandler{poolSvc: poolSvc}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Response types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type poolJSON struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Notes *string `json:"notes"`
|
|
CreatorID int16 `json:"creator_id"`
|
|
CreatorName string `json:"creator_name"`
|
|
IsPublic bool `json:"is_public"`
|
|
FileCount int `json:"file_count"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
type poolFileJSON struct {
|
|
fileJSON
|
|
Position int `json:"position"`
|
|
}
|
|
|
|
func toPoolJSON(p domain.Pool) poolJSON {
|
|
return poolJSON{
|
|
ID: p.ID.String(),
|
|
Name: p.Name,
|
|
Notes: p.Notes,
|
|
CreatorID: p.CreatorID,
|
|
CreatorName: p.CreatorName,
|
|
IsPublic: p.IsPublic,
|
|
FileCount: p.FileCount,
|
|
CreatedAt: p.CreatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func toPoolFileJSON(pf domain.PoolFile) poolFileJSON {
|
|
return poolFileJSON{
|
|
fileJSON: toFileJSON(pf.File),
|
|
Position: pf.Position,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func parsePoolID(c *gin.Context) (uuid.UUID, bool) {
|
|
id, err := uuid.Parse(c.Param("pool_id"))
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return uuid.UUID{}, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
func parsePoolFileParams(c *gin.Context) port.PoolFileListParams {
|
|
limit := 50
|
|
if s := c.Query("limit"); s != "" {
|
|
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 200 {
|
|
limit = n
|
|
}
|
|
}
|
|
return port.PoolFileListParams{
|
|
Cursor: c.Query("cursor"),
|
|
Limit: limit,
|
|
Filter: c.Query("filter"),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /pools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) List(c *gin.Context) {
|
|
params := parseOffsetParams(c, "created")
|
|
|
|
page, err := h.poolSvc.List(c.Request.Context(), params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
items := make([]poolJSON, len(page.Items))
|
|
for i, p := range page.Items {
|
|
items[i] = toPoolJSON(p)
|
|
}
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": page.Total,
|
|
"offset": page.Offset,
|
|
"limit": page.Limit,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /pools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) Create(c *gin.Context) {
|
|
var body struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Notes *string `json:"notes"`
|
|
IsPublic *bool `json:"is_public"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
created, err := h.poolSvc.Create(c.Request.Context(), service.PoolParams{
|
|
Name: body.Name,
|
|
Notes: body.Notes,
|
|
IsPublic: body.IsPublic,
|
|
})
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusCreated, toPoolJSON(*created))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /pools/:pool_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) Get(c *gin.Context) {
|
|
id, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
p, err := h.poolSvc.Get(c.Request.Context(), id)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusOK, toPoolJSON(*p))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PATCH /pools/:pool_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) Update(c *gin.Context) {
|
|
id, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var raw map[string]any
|
|
if err := c.ShouldBindJSON(&raw); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
params := service.PoolParams{}
|
|
if v, ok := raw["name"]; ok {
|
|
if s, ok := v.(string); ok {
|
|
params.Name = s
|
|
}
|
|
}
|
|
if _, ok := raw["notes"]; ok {
|
|
if raw["notes"] == nil {
|
|
empty := ""
|
|
params.Notes = &empty
|
|
} else if s, ok := raw["notes"].(string); ok {
|
|
params.Notes = &s
|
|
}
|
|
}
|
|
if v, ok := raw["is_public"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
params.IsPublic = &b
|
|
}
|
|
}
|
|
|
|
updated, err := h.poolSvc.Update(c.Request.Context(), id, params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusOK, toPoolJSON(*updated))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DELETE /pools/:pool_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) Delete(c *gin.Context) {
|
|
id, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if err := h.poolSvc.Delete(c.Request.Context(), id); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /pools/:pool_id/files
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) ListFiles(c *gin.Context) {
|
|
poolID, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
params := parsePoolFileParams(c)
|
|
|
|
page, err := h.poolSvc.ListFiles(c.Request.Context(), poolID, params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
items := make([]poolFileJSON, len(page.Items))
|
|
for i, pf := range page.Items {
|
|
items[i] = toPoolFileJSON(pf)
|
|
}
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"items": items,
|
|
"next_cursor": page.NextCursor,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /pools/:pool_id/files
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) AddFiles(c *gin.Context) {
|
|
poolID, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
FileIDs []string `json:"file_ids" binding:"required"`
|
|
Position *int `json:"position"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
fileIDs := make([]uuid.UUID, 0, len(body.FileIDs))
|
|
for _, s := range body.FileIDs {
|
|
id, err := uuid.Parse(s)
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
fileIDs = append(fileIDs, id)
|
|
}
|
|
|
|
if err := h.poolSvc.AddFiles(c.Request.Context(), poolID, fileIDs, body.Position); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusCreated)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /pools/:pool_id/files/remove
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) RemoveFiles(c *gin.Context) {
|
|
poolID, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
FileIDs []string `json:"file_ids" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
fileIDs := make([]uuid.UUID, 0, len(body.FileIDs))
|
|
for _, s := range body.FileIDs {
|
|
id, err := uuid.Parse(s)
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
fileIDs = append(fileIDs, id)
|
|
}
|
|
|
|
if err := h.poolSvc.RemoveFiles(c.Request.Context(), poolID, fileIDs); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PUT /pools/:pool_id/files/reorder
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *PoolHandler) Reorder(c *gin.Context) {
|
|
poolID, ok := parsePoolID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
FileIDs []string `json:"file_ids" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
fileIDs := make([]uuid.UUID, 0, len(body.FileIDs))
|
|
for _, s := range body.FileIDs {
|
|
id, err := uuid.Parse(s)
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
fileIDs = append(fileIDs, id)
|
|
}
|
|
|
|
if err := h.poolSvc.Reorder(c.Request.Context(), poolID, fileIDs); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
} |