275 lines
9.0 KiB
Go
275 lines
9.0 KiB
Go
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"]),
|
|
}
|
|
}
|
|
|
|
// handleV1GetCategory serves GET /api/v1/categories/{id} as the public list DTO
|
|
// (no prompts, formulas, or has_prompt). Dashboard GET stays on handleGetCategory.
|
|
func (s *Server) handleV1GetCategory(w http.ResponseWriter, r *http.Request) {
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
|
return
|
|
}
|
|
item, err := s.Catalog.GetCategory(r.Context(), cid, id)
|
|
if err != nil {
|
|
Error(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
v1OK(w, http.StatusOK, presentV1Category(item), nil)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|