package entry import ( "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/claims" "RenewCMS/api/pkg/response" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/contenttype" domain "RenewCMS/internal/domain/entry" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "net/http" "net/url" "strconv" "time" "github.com/go-chi/chi/v5" ) type Handler struct { CreateUseCase *useCases.CreateEntryUseCase GetUseCase *useCases.GetEntryUseCase ListUseCase *useCases.ListEntriesUseCase UpdateUseCase *useCases.UpdateEntryUseCase DeleteUseCase *useCases.DeleteEntryUseCase } func NewHandler(create *useCases.CreateEntryUseCase, get *useCases.GetEntryUseCase, list *useCases.ListEntriesUseCase, update *useCases.UpdateEntryUseCase, deleteUseCase *useCases.DeleteEntryUseCase) *Handler { return &Handler{CreateUseCase: create, GetUseCase: get, ListUseCase: list, UpdateUseCase: update, DeleteUseCase: deleteUseCase} } type EntryBody struct { Data map[string]any `json:"data" validate:"required"` } type entryResponse struct { ID uint32 `json:"id"` ContentType string `json:"content_type"` Data map[string]any `json:"data"` CreatedBy uint32 `json:"created_by"` UpdatedBy uint32 `json:"updated_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func toResponse(typed useCases.TypedEntry) entryResponse { return entryResponse{ ID: typed.ID, ContentType: typed.ContentTypeKey, Data: typed.Data, CreatedBy: typed.CreatedBy, UpdatedBy: typed.UpdatedBy, CreatedAt: typed.CreatedAt, UpdatedAt: typed.UpdatedAt, } } type pageResponse struct { Items []entryResponse `json:"items"` Total int `json:"total"` Page int `json:"page"` Size int `json:"size"` } const ( defaultListPage = 1 defaultListSize = 25 ) type listQuery struct { Page int `validate:"gte=1"` Size int `validate:"oneof=5 25 50 100"` Sort domain.Sort } func intQueryParam(values url.Values, name string, fallback int) (int, error) { if !values.Has(name) { return fallback, nil } return strconv.Atoi(values.Get(name)) } func readListQuery(values url.Values) (listQuery, error) { page, err := intQueryParam(values, "page", defaultListPage) if err != nil { return listQuery{}, err } size, err := intQueryParam(values, "size", defaultListSize) if err != nil { return listQuery{}, err } order := domain.DefaultSort() if values.Has("sort") { order, err = domain.ParseSort(values.Get("sort")) if err != nil { return listQuery{}, err } } return listQuery{Page: page, Size: size, Sort: order}, nil } func parseEntryId(r *http.Request) (uint32, error) { id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) return uint32(id), err } func writeEntryError(w http.ResponseWriter, err error) { var fieldError domain.FieldError switch { case errors.Is(err, contenttype.ErrContentTypeNotFound): http.Error(w, apperr.ContentTypeNotFoundErrorMessage, http.StatusNotFound) case errors.Is(err, domain.ErrEntryNotFound): http.Error(w, apperr.EntryNotFoundErrorMessage, http.StatusNotFound) case errors.As(err, &fieldError): http.Error(w, apperr.InvalidEntryErrorMessage+": "+fieldError.Error(), fieldErrorStatus(err)) case errors.Is(err, domain.ErrInvalidPage), errors.Is(err, domain.ErrInvalidSort): http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) case errors.Is(err, domain.ErrInvalidEntry): http.Error(w, apperr.InvalidEntryErrorMessage, http.StatusUnprocessableEntity) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } } func fieldErrorStatus(err error) int { if errors.Is(err, domain.ErrUniqueViolation) { return http.StatusConflict } return http.StatusUnprocessableEntity } func readBody(w http.ResponseWriter, r *http.Request) (EntryBody, bool) { var body EntryBody if err := json.NewDecoder(r.Body).Decode(&body); err != nil || validator.Validate.Struct(body) != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return EntryBody{}, false } return body, true } func currentUserId(w http.ResponseWriter, r *http.Request) (uint32, bool) { userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) } return userId, ok } func (h *Handler) List(w http.ResponseWriter, r *http.Request) { query, err := readListQuery(r.URL.Query()) if err != nil || validator.Validate.Struct(query) != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } page, err := h.ListUseCase.ListPage(useCases.ListEntriesCommand{ContentTypeKey: chi.URLParam(r, "key"), Page: query.Page, Size: query.Size, Sort: query.Sort}) if err != nil { writeEntryError(w, err) return } items := make([]entryResponse, 0, len(page.Items)) for _, typed := range page.Items { items = append(items, toResponse(typed)) } response.WriteJSON(w, http.StatusOK, pageResponse{Items: items, Total: page.Total, Page: page.Page, Size: page.Size}) } func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { id, err := parseEntryId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } found, err := h.GetUseCase.Get(chi.URLParam(r, "key"), id) if err != nil { writeEntryError(w, err) return } response.WriteJSON(w, http.StatusOK, toResponse(found)) } func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { userId, ok := currentUserId(w, r) if !ok { return } body, ok := readBody(w, r) if !ok { return } created, err := h.CreateUseCase.Create(useCases.CreateEntryCommand{ContentTypeKey: chi.URLParam(r, "key"), Data: body.Data, UserID: userId}) if err != nil { writeEntryError(w, err) return } response.WriteJSON(w, http.StatusCreated, toResponse(created)) } func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { id, err := parseEntryId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } userId, ok := currentUserId(w, r) if !ok { return } body, ok := readBody(w, r) if !ok { return } updated, err := h.UpdateUseCase.Update(useCases.UpdateEntryCommand{ContentTypeKey: chi.URLParam(r, "key"), ID: id, Data: body.Data, UserID: userId}) if err != nil { writeEntryError(w, err) return } response.WriteJSON(w, http.StatusOK, toResponse(updated)) } func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { id, err := parseEntryId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } if err := h.DeleteUseCase.Delete(chi.URLParam(r, "key"), id); err != nil { writeEntryError(w, err) return } w.WriteHeader(http.StatusNoContent) }