Add category repo, service, handler, and wire all /categories endpoints including list, create, get, update, delete, and list-tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
235 lines
5.9 KiB
Go
235 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"tanabata/backend/internal/domain"
|
|
"tanabata/backend/internal/service"
|
|
)
|
|
|
|
// CategoryHandler handles all /categories endpoints.
|
|
type CategoryHandler struct {
|
|
categorySvc *service.CategoryService
|
|
}
|
|
|
|
// NewCategoryHandler creates a CategoryHandler.
|
|
func NewCategoryHandler(categorySvc *service.CategoryService) *CategoryHandler {
|
|
return &CategoryHandler{categorySvc: categorySvc}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Response types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type categoryJSON struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Notes *string `json:"notes"`
|
|
Color *string `json:"color"`
|
|
CreatorID int16 `json:"creator_id"`
|
|
CreatorName string `json:"creator_name"`
|
|
IsPublic bool `json:"is_public"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
func toCategoryJSON(c domain.Category) categoryJSON {
|
|
return categoryJSON{
|
|
ID: c.ID.String(),
|
|
Name: c.Name,
|
|
Notes: c.Notes,
|
|
Color: c.Color,
|
|
CreatorID: c.CreatorID,
|
|
CreatorName: c.CreatorName,
|
|
IsPublic: c.IsPublic,
|
|
CreatedAt: c.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func parseCategoryID(c *gin.Context) (uuid.UUID, bool) {
|
|
id, err := uuid.Parse(c.Param("category_id"))
|
|
if err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return uuid.UUID{}, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) List(c *gin.Context) {
|
|
params := parseOffsetParams(c, "created")
|
|
|
|
page, err := h.categorySvc.List(c.Request.Context(), params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
items := make([]categoryJSON, len(page.Items))
|
|
for i, cat := range page.Items {
|
|
items[i] = toCategoryJSON(cat)
|
|
}
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": page.Total,
|
|
"offset": page.Offset,
|
|
"limit": page.Limit,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) Create(c *gin.Context) {
|
|
var body struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Notes *string `json:"notes"`
|
|
Color *string `json:"color"`
|
|
IsPublic *bool `json:"is_public"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
created, err := h.categorySvc.Create(c.Request.Context(), service.CategoryParams{
|
|
Name: body.Name,
|
|
Notes: body.Notes,
|
|
Color: body.Color,
|
|
IsPublic: body.IsPublic,
|
|
})
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusCreated, toCategoryJSON(*created))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /categories/:category_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) Get(c *gin.Context) {
|
|
id, ok := parseCategoryID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cat, err := h.categorySvc.Get(c.Request.Context(), id)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusOK, toCategoryJSON(*cat))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PATCH /categories/:category_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) Update(c *gin.Context) {
|
|
id, ok := parseCategoryID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Use a raw map to detect explicitly-null fields.
|
|
var raw map[string]any
|
|
if err := c.ShouldBindJSON(&raw); err != nil {
|
|
respondError(c, domain.ErrValidation)
|
|
return
|
|
}
|
|
|
|
params := service.CategoryParams{}
|
|
|
|
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 _, ok := raw["color"]; ok {
|
|
if raw["color"] == nil {
|
|
empty := ""
|
|
params.Color = &empty
|
|
} else if s, ok := raw["color"].(string); ok {
|
|
params.Color = &s
|
|
}
|
|
}
|
|
if v, ok := raw["is_public"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
params.IsPublic = &b
|
|
}
|
|
}
|
|
|
|
updated, err := h.categorySvc.Update(c.Request.Context(), id, params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
respondJSON(c, http.StatusOK, toCategoryJSON(*updated))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DELETE /categories/:category_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) Delete(c *gin.Context) {
|
|
id, ok := parseCategoryID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if err := h.categorySvc.Delete(c.Request.Context(), id); err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /categories/:category_id/tags
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (h *CategoryHandler) ListTags(c *gin.Context) {
|
|
id, ok := parseCategoryID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
params := parseOffsetParams(c, "created")
|
|
|
|
page, err := h.categorySvc.ListTags(c.Request.Context(), id, params)
|
|
if err != nil {
|
|
respondError(c, err)
|
|
return
|
|
}
|
|
|
|
items := make([]tagJSON, len(page.Items))
|
|
for i, t := range page.Items {
|
|
items[i] = toTagJSON(t)
|
|
}
|
|
respondJSON(c, http.StatusOK, gin.H{
|
|
"items": items,
|
|
"total": page.Total,
|
|
"offset": page.Offset,
|
|
"limit": page.Limit,
|
|
})
|
|
} |