feat: implement full tag stack (repo, service, handler, routes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,12 @@ import (
|
||||
// FileHandler handles all /files endpoints.
|
||||
type FileHandler struct {
|
||||
fileSvc *service.FileService
|
||||
tagSvc *service.TagService
|
||||
}
|
||||
|
||||
// NewFileHandler creates a FileHandler.
|
||||
func NewFileHandler(fileSvc *service.FileService) *FileHandler {
|
||||
return &FileHandler{fileSvc: fileSvc}
|
||||
func NewFileHandler(fileSvc *service.FileService, tagSvc *service.TagService) *FileHandler {
|
||||
return &FileHandler{fileSvc: fileSvc, tagSvc: tagSvc}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -498,117 +499,6 @@ func (h *FileHandler) PermanentDelete(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id/tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) ListTags(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.ListFileTags(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /files/:id/tags (replace all)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) SetTags(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
TagIDs []string `json:"tag_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs, err := parseUUIDs(body.TagIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.SetFileTags(c.Request.Context(), id, tagIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /files/:id/tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) AddTag(c *gin.Context) {
|
||||
fileID, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.AddTag(c.Request.Context(), fileID, tagID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /files/:id/tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) RemoveTag(c *gin.Context) {
|
||||
fileID, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.fileSvc.RemoveTag(c.Request.Context(), fileID, tagID); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/bulk/tags
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -639,7 +529,7 @@ func (h *FileHandler) BulkSetTags(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
applied, err := h.fileSvc.BulkSetTags(c.Request.Context(), fileIDs, body.Action, tagIDs)
|
||||
applied, err := h.tagSvc.BulkSetTags(c.Request.Context(), fileIDs, body.Action, tagIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -698,16 +588,16 @@ func (h *FileHandler) CommonTags(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
common, partial, err := h.fileSvc.CommonTags(c.Request.Context(), fileIDs)
|
||||
common, partial, err := h.tagSvc.CommonTags(c.Request.Context(), fileIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
toStrs := func(ids []uuid.UUID) []string {
|
||||
s := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
s[i] = id.String()
|
||||
toStrs := func(tags []domain.Tag) []string {
|
||||
s := make([]string, len(tags))
|
||||
for i, t := range tags {
|
||||
s[i] = t.ID.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// NewRouter builds and returns a configured Gin engine.
|
||||
func NewRouter(auth *AuthMiddleware, authHandler *AuthHandler, fileHandler *FileHandler) *gin.Engine {
|
||||
func NewRouter(
|
||||
auth *AuthMiddleware,
|
||||
authHandler *AuthHandler,
|
||||
fileHandler *FileHandler,
|
||||
tagHandler *TagHandler,
|
||||
) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(gin.Logger(), gin.Recovery())
|
||||
|
||||
@@ -18,7 +23,9 @@ func NewRouter(auth *AuthMiddleware, authHandler *AuthHandler, fileHandler *File
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
|
||||
// Auth endpoints — login and refresh are public; others require a valid token.
|
||||
// -------------------------------------------------------------------------
|
||||
// Auth
|
||||
// -------------------------------------------------------------------------
|
||||
authGroup := v1.Group("/auth")
|
||||
{
|
||||
authGroup.POST("/login", authHandler.Login)
|
||||
@@ -32,13 +39,15 @@ func NewRouter(auth *AuthMiddleware, authHandler *AuthHandler, fileHandler *File
|
||||
}
|
||||
}
|
||||
|
||||
// File endpoints — all require authentication.
|
||||
// -------------------------------------------------------------------------
|
||||
// Files (all require auth)
|
||||
// -------------------------------------------------------------------------
|
||||
files := v1.Group("/files", auth.Handle())
|
||||
{
|
||||
files.GET("", fileHandler.List)
|
||||
files.POST("", fileHandler.Upload)
|
||||
|
||||
// Bulk routes must be registered before /:id to avoid ambiguity.
|
||||
// Bulk + import routes registered before /:id to prevent param collision.
|
||||
files.POST("/bulk/tags", fileHandler.BulkSetTags)
|
||||
files.POST("/bulk/delete", fileHandler.BulkDelete)
|
||||
files.POST("/bulk/common-tags", fileHandler.CommonTags)
|
||||
@@ -56,10 +65,30 @@ func NewRouter(auth *AuthMiddleware, authHandler *AuthHandler, fileHandler *File
|
||||
files.POST("/:id/restore", fileHandler.Restore)
|
||||
files.DELETE("/:id/permanent", fileHandler.PermanentDelete)
|
||||
|
||||
files.GET("/:id/tags", fileHandler.ListTags)
|
||||
files.PUT("/:id/tags", fileHandler.SetTags)
|
||||
files.PUT("/:id/tags/:tag_id", fileHandler.AddTag)
|
||||
files.DELETE("/:id/tags/:tag_id", fileHandler.RemoveTag)
|
||||
// File–tag relations — served by TagHandler for auto-rule support.
|
||||
files.GET("/:id/tags", tagHandler.FileListTags)
|
||||
files.PUT("/:id/tags", tagHandler.FileSetTags)
|
||||
files.PUT("/:id/tags/:tag_id", tagHandler.FileAddTag)
|
||||
files.DELETE("/:id/tags/:tag_id", tagHandler.FileRemoveTag)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tags (all require auth)
|
||||
// -------------------------------------------------------------------------
|
||||
tags := v1.Group("/tags", auth.Handle())
|
||||
{
|
||||
tags.GET("", tagHandler.List)
|
||||
tags.POST("", tagHandler.Create)
|
||||
|
||||
tags.GET("/:tag_id", tagHandler.Get)
|
||||
tags.PATCH("/:tag_id", tagHandler.Update)
|
||||
tags.DELETE("/:tag_id", tagHandler.Delete)
|
||||
|
||||
tags.GET("/:tag_id/files", tagHandler.ListFiles)
|
||||
|
||||
tags.GET("/:tag_id/rules", tagHandler.ListRules)
|
||||
tags.POST("/:tag_id/rules", tagHandler.CreateRule)
|
||||
tags.DELETE("/:tag_id/rules/:then_tag_id", tagHandler.DeleteRule)
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"tanabata/backend/internal/domain"
|
||||
"tanabata/backend/internal/port"
|
||||
"tanabata/backend/internal/service"
|
||||
)
|
||||
|
||||
// TagHandler handles all /tags endpoints.
|
||||
type TagHandler struct {
|
||||
tagSvc *service.TagService
|
||||
fileSvc *service.FileService
|
||||
}
|
||||
|
||||
// NewTagHandler creates a TagHandler.
|
||||
func NewTagHandler(tagSvc *service.TagService, fileSvc *service.FileService) *TagHandler {
|
||||
return &TagHandler{tagSvc: tagSvc, fileSvc: fileSvc}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type tagRuleJSON struct {
|
||||
WhenTagID string `json:"when_tag_id"`
|
||||
ThenTagID string `json:"then_tag_id"`
|
||||
ThenTagName string `json:"then_tag_name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func toTagRuleJSON(r domain.TagRule) tagRuleJSON {
|
||||
return tagRuleJSON{
|
||||
WhenTagID: r.WhenTagID.String(),
|
||||
ThenTagID: r.ThenTagID.String(),
|
||||
ThenTagName: r.ThenTagName,
|
||||
IsActive: r.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseTagID(c *gin.Context) (uuid.UUID, bool) {
|
||||
id, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func parseOffsetParams(c *gin.Context, defaultSort string) port.OffsetParams {
|
||||
limit := 50
|
||||
if s := c.Query("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 200 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
offset := 0
|
||||
if s := c.Query("offset"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n >= 0 {
|
||||
offset = n
|
||||
}
|
||||
}
|
||||
sort := c.DefaultQuery("sort", defaultSort)
|
||||
order := c.DefaultQuery("order", "desc")
|
||||
search := c.Query("search")
|
||||
return port.OffsetParams{Sort: sort, Order: order, Search: search, Limit: limit, Offset: offset}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) List(c *gin.Context) {
|
||||
params := parseOffsetParams(c, "created")
|
||||
|
||||
page, err := h.tagSvc.List(c.Request.Context(), 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,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) Create(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Notes *string `json:"notes"`
|
||||
Color *string `json:"color"`
|
||||
CategoryID *string `json:"category_id"`
|
||||
IsPublic *bool `json:"is_public"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
params := service.TagParams{
|
||||
Name: body.Name,
|
||||
Notes: body.Notes,
|
||||
Color: body.Color,
|
||||
IsPublic: body.IsPublic,
|
||||
}
|
||||
if body.CategoryID != nil {
|
||||
id, err := uuid.Parse(*body.CategoryID)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.CategoryID = &id
|
||||
}
|
||||
|
||||
t, err := h.tagSvc.Create(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusCreated, toTagJSON(*t))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) Get(c *gin.Context) {
|
||||
id, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := h.tagSvc.Get(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toTagJSON(*t))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATCH /tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) Update(c *gin.Context) {
|
||||
id, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Use a raw map to distinguish "field absent" from "field = null".
|
||||
var raw map[string]any
|
||||
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
params := service.TagParams{}
|
||||
|
||||
if v, ok := raw["name"]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
params.Name = s
|
||||
}
|
||||
}
|
||||
if _, ok := raw["notes"]; ok {
|
||||
if raw["notes"] == nil {
|
||||
params.Notes = ptr("")
|
||||
} else if s, ok := raw["notes"].(string); ok {
|
||||
params.Notes = &s
|
||||
}
|
||||
}
|
||||
if _, ok := raw["color"]; ok {
|
||||
if raw["color"] == nil {
|
||||
nilStr := ""
|
||||
params.Color = &nilStr
|
||||
} else if s, ok := raw["color"].(string); ok {
|
||||
params.Color = &s
|
||||
}
|
||||
}
|
||||
if _, ok := raw["category_id"]; ok {
|
||||
if raw["category_id"] == nil {
|
||||
nilID := uuid.Nil
|
||||
params.CategoryID = &nilID // signals "unassign"
|
||||
} else if s, ok := raw["category_id"].(string); ok {
|
||||
cid, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.CategoryID = &cid
|
||||
}
|
||||
}
|
||||
if v, ok := raw["is_public"]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
params.IsPublic = &b
|
||||
}
|
||||
}
|
||||
|
||||
t, err := h.tagSvc.Update(c.Request.Context(), id, params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toTagJSON(*t))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) Delete(c *gin.Context) {
|
||||
id, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tagSvc.Delete(c.Request.Context(), id); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /tags/:tag_id/files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) ListFiles(c *gin.Context) {
|
||||
id, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if s := c.Query("limit"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 200 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to file service with a tag filter.
|
||||
page, err := h.fileSvc.List(c.Request.Context(), domain.FileListParams{
|
||||
Cursor: c.Query("cursor"),
|
||||
Direction: "forward",
|
||||
Limit: limit,
|
||||
Sort: "created",
|
||||
Order: "desc",
|
||||
Filter: "{t=" + id.String() + "}",
|
||||
})
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]fileJSON, len(page.Items))
|
||||
for i, f := range page.Items {
|
||||
items[i] = toFileJSON(f)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, gin.H{
|
||||
"items": items,
|
||||
"next_cursor": page.NextCursor,
|
||||
"prev_cursor": page.PrevCursor,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /tags/:tag_id/rules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) ListRules(c *gin.Context) {
|
||||
id, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rules, err := h.tagSvc.ListRules(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagRuleJSON, len(rules))
|
||||
for i, r := range rules {
|
||||
items[i] = toTagRuleJSON(r)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /tags/:tag_id/rules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) CreateRule(c *gin.Context) {
|
||||
whenTagID, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
ThenTagID string `json:"then_tag_id" binding:"required"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
ApplyToExisting *bool `json:"apply_to_existing"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
thenTagID, err := uuid.Parse(body.ThenTagID)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
if body.IsActive != nil {
|
||||
isActive = *body.IsActive
|
||||
}
|
||||
applyToExisting := true
|
||||
if body.ApplyToExisting != nil {
|
||||
applyToExisting = *body.ApplyToExisting
|
||||
}
|
||||
|
||||
rule, err := h.tagSvc.CreateRule(c.Request.Context(), whenTagID, thenTagID, isActive, applyToExisting)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusCreated, toTagRuleJSON(*rule))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /tags/:tag_id/rules/:then_tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *TagHandler) DeleteRule(c *gin.Context) {
|
||||
whenTagID, ok := parseTagID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
thenTagID, err := uuid.Parse(c.Param("then_tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tagSvc.DeleteRule(c.Request.Context(), whenTagID, thenTagID); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File-tag endpoints wired through TagService
|
||||
// (called from file routes, shared handler logic lives here)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// FileListTags handles GET /files/:id/tags.
|
||||
func (h *TagHandler) FileListTags(c *gin.Context) {
|
||||
fileID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.tagSvc.ListFileTags(c.Request.Context(), fileID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// FileSetTags handles PUT /files/:id/tags.
|
||||
func (h *TagHandler) FileSetTags(c *gin.Context) {
|
||||
fileID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
TagIDs []string `json:"tag_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs, err := parseUUIDs(body.TagIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.tagSvc.SetFileTags(c.Request.Context(), fileID, tagIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// FileAddTag handles PUT /files/:id/tags/:tag_id.
|
||||
func (h *TagHandler) FileAddTag(c *gin.Context) {
|
||||
fileID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.tagSvc.AddFileTag(c.Request.Context(), fileID, tagID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// FileRemoveTag handles DELETE /files/:id/tags/:tag_id.
|
||||
func (h *TagHandler) FileRemoveTag(c *gin.Context) {
|
||||
fileID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tagSvc.RemoveFileTag(c.Request.Context(), fileID, tagID); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func ptr(s string) *string { return &s }
|
||||
Reference in New Issue
Block a user