Files
descrybe/apps/api/internal/httpapi/catalog_v1_handlers.go
T

258 lines
8.4 KiB
Go
Raw Normal View History

package httpapi
import (
"errors"
"net/http"
"strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
func v1CatalogListMeta(page, limit int, total int64) map[string]any {
return v1ProductListMeta(page, limit, total)
}
func presentV1Category(item map[string]any) map[string]any {
return map[string]any{
"id": item["id"],
"unique_id": item["unique_id"],
"name": item["name"],
"created_at": formatV1Timestamp(item["created_at"]),
"updated_at": formatV1Timestamp(item["updated_at"]),
}
}
func presentV1Attribute(item map[string]any) map[string]any {
out := map[string]any{
"id": item["id"],
"key": item["attribute_key"],
"name": item["name"],
"type": item["value_type"],
"unit": item["unit"],
"required": false,
"created_at": formatV1Timestamp(item["created_at"]),
"updated_at": formatV1Timestamp(item["updated_at"]),
}
if v, ok := item["required"]; ok && v != nil {
switch t := v.(type) {
case bool:
out["required"] = t
}
}
if cid, ok := item["category_unique_id"]; ok && cid != nil && asMapString(cid) != "" {
out["category_unique_id"] = cid
}
return out
}
// handleV1ListCategories serves GET /api/v1/categories with legacy { data, meta }.
func (s *Server) handleV1ListCategories(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
page, limit, offset := ParsePageLimit(r)
items, total, err := s.Catalog.ListCategories(r.Context(), cid, catalog.ListFilter{
Query: QuerySearch(r),
Limit: limit,
Offset: offset,
})
if err != nil {
Error(w, http.StatusInternalServerError, "list failed")
return
}
data := make([]map[string]any, 0, len(items))
for _, item := range items {
data = append(data, presentV1Category(item))
}
v1OK(w, http.StatusOK, data, v1CatalogListMeta(page, limit, total))
}
// handleV1CreateCategory serves POST /api/v1/categories and /categories/create.
// Body: name + unique_id required; parent_id alias for parent_unique_id.
func (s *Server) handleV1CreateCategory(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
var body struct {
Name string `json:"name"`
UniqueID string `json:"unique_id"`
ParentUniqueID *string `json:"parent_unique_id"`
ParentID *string `json:"parent_id"`
Description *string `json:"description"`
}
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
parent := body.ParentUniqueID
if (parent == nil || strings.TrimSpace(*parent) == "") && body.ParentID != nil {
parent = body.ParentID
}
item, err := s.Catalog.CreateCategory(r.Context(), cid, body.Name, body.UniqueID, parent, body.Description)
if err != nil {
if msg, ok := catalog.ClientError(err); ok {
Error(w, http.StatusBadRequest, msg)
return
}
ClientOrLog(w, http.StatusBadRequest, "could not create category", err, catalog.ClientError)
return
}
v1OK(w, http.StatusCreated, map[string]any{
"id": item["id"],
"unique_id": item["unique_id"],
"name": item["name"],
}, nil)
}
// handleV1DeleteCategory serves DELETE /api/v1/categories/{id} where {id} is unique_id.
func (s *Server) handleV1DeleteCategory(w http.ResponseWriter, r *http.Request) {
if !requireCompanyAdmin(w, r) {
return
}
cid, _ := CompanyIDFromContext(r.Context())
uniqueID := strings.TrimSpace(chi.URLParam(r, "id"))
if uniqueID == "" {
Error(w, http.StatusBadRequest, "invalid category id")
return
}
if err := s.Catalog.DeleteCategoryByUniqueID(r.Context(), cid, uniqueID); err != nil {
if errors.Is(err, catalog.ErrNotFound) {
Error(w, http.StatusNotFound, "not found")
return
}
if msg, ok := catalog.ClientError(err); ok {
Error(w, http.StatusBadRequest, msg)
return
}
Error(w, http.StatusInternalServerError, "delete failed")
return
}
v1OK(w, http.StatusOK, map[string]any{"message": "Category deleted successfully"}, nil)
}
// handleV1ListAttributes serves GET /api/v1/attributes with legacy { data, meta }.
func (s *Server) handleV1ListAttributes(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
page, limit, offset := ParsePageLimit(r)
f := catalog.ListFilter{
Query: QuerySearch(r),
Limit: limit,
Offset: offset,
Category: firstNonEmpty(r.URL.Query().Get("categoryId"), r.URL.Query().Get("category_id")),
RootsOnly: r.URL.Query().Get("roots") == "1",
ParentKey: firstNonEmpty(r.URL.Query().Get("parent_key"), r.URL.Query().Get("parentKey")),
SortBy: firstNonEmpty(r.URL.Query().Get("sortBy"), r.URL.Query().Get("sort_by"), "updatedAt"),
SortOrder: firstNonEmpty(r.URL.Query().Get("sortOrder"), r.URL.Query().Get("sort_order"), "desc"),
}
items, total, err := s.Catalog.ListAttributes(r.Context(), cid, f)
if err != nil {
Error(w, http.StatusInternalServerError, "list failed")
return
}
data := make([]map[string]any, 0, len(items))
for _, item := range items {
data = append(data, presentV1Attribute(item))
}
v1OK(w, http.StatusOK, data, v1CatalogListMeta(page, limit, total))
}
// handleV1CreateAttribute serves POST /api/v1/attributes and /attributes/create.
// Requires name, attribute_key, value_type, category_unique_id (legacy contract).
func (s *Server) handleV1CreateAttribute(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
var body struct {
Name string `json:"name"`
AttributeKey string `json:"attribute_key"`
ValueType string `json:"value_type"`
Unit *string `json:"unit"`
Example *string `json:"example"`
ParentKey *string `json:"parent_key"`
CategoryUniqueID string `json:"category_unique_id"`
Required bool `json:"required"`
}
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
if strings.TrimSpace(body.Name) == "" || strings.TrimSpace(body.AttributeKey) == "" ||
strings.TrimSpace(body.ValueType) == "" || strings.TrimSpace(body.CategoryUniqueID) == "" {
Error(w, http.StatusBadRequest, "Missing required fields: name, attribute_key, value_type, category_unique_id")
return
}
item, err := s.Catalog.CreateAttribute(r.Context(), cid, body.AttributeKey, body.Name, body.ValueType, body.Unit, body.Example, body.ParentKey)
if err != nil {
if msg, ok := catalog.ClientError(err); ok {
Error(w, http.StatusBadRequest, msg)
return
}
ClientOrLog(w, http.StatusBadRequest, "could not create attribute", err, catalog.ClientError)
return
}
attrID, err := parseMapUUID(item["id"])
if err != nil {
Error(w, http.StatusInternalServerError, "could not create attribute")
return
}
if _, err := s.Catalog.LinkCategoryAttribute(r.Context(), cid, body.CategoryUniqueID, attrID, body.Required); err != nil {
if msg, ok := catalog.ClientError(err); ok {
status := http.StatusBadRequest
if strings.Contains(strings.ToLower(msg), "not found") {
status = http.StatusNotFound
}
Error(w, status, msg)
return
}
ClientOrLog(w, http.StatusBadRequest, "could not link attribute", err, catalog.ClientError)
return
}
v1OK(w, http.StatusCreated, map[string]any{
"id": item["id"],
"key": item["attribute_key"],
"name": item["name"],
"type": item["value_type"],
"unit": item["unit"],
"category_unique_id": body.CategoryUniqueID,
"required": body.Required,
}, nil)
}
// handleV1DeleteAttribute serves DELETE /api/v1/attributes/{id} (UUID).
func (s *Server) handleV1DeleteAttribute(w http.ResponseWriter, r *http.Request) {
if !requireCompanyAdmin(w, r) {
return
}
cid, _ := CompanyIDFromContext(r.Context())
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
Error(w, http.StatusBadRequest, "invalid attribute id")
return
}
if err := s.Catalog.DeleteAttribute(r.Context(), cid, id); err != nil {
if errors.Is(err, catalog.ErrNotFound) {
Error(w, http.StatusNotFound, "not found")
return
}
Error(w, http.StatusInternalServerError, "delete failed")
return
}
v1OK(w, http.StatusOK, map[string]any{"message": "Attribute deleted successfully"}, nil)
}
func parseMapUUID(v any) (uuid.UUID, error) {
switch t := v.(type) {
case uuid.UUID:
return t, nil
case string:
return uuid.Parse(t)
case [16]byte:
return uuid.UUID(t), nil
default:
s := asMapString(v)
if s == "" {
return uuid.Nil, errors.New("invalid uuid")
}
return uuid.Parse(s)
}
}