Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *Server) handleListCategories(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
max := maxPageLimit
|
||||
if r.URL.Query().Get("tree") == "1" {
|
||||
max = maxTreePageLimit
|
||||
}
|
||||
limit, offset := ParseLimitOffsetMax(r, max)
|
||||
f := catalog.ListFilter{
|
||||
Query: QuerySearch(r),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
items, total, err := s.Catalog.ListCategories(r.Context(), cid, f)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]any{"categories": items, "total": total, "limit": limit, "offset": offset})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateCategory(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"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.CreateCategory(r.Context(), cid, body.Name, body.UniqueID, body.ParentUniqueID, body.Description)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not create category", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetCategory(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
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateCategory(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
|
||||
}
|
||||
var body map[string]any
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.UpdateCategory(r.Context(), cid, id, body)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update category", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteCategory(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 id")
|
||||
return
|
||||
}
|
||||
if err := s.Catalog.DeleteCategory(r.Context(), cid, id); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "delete failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateTitleFormula(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
|
||||
}
|
||||
var body struct {
|
||||
TitleTemplate any `json:"title_template"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.UpdateTitleFormula(r.Context(), cid, id, body.TitleTemplate)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update title formula", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateDescriptionFormula(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
|
||||
}
|
||||
var body struct {
|
||||
DescriptionTemplate any `json:"description_template"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.UpdateDescriptionFormula(r.Context(), cid, id, body.DescriptionTemplate)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update description formula", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateCategoryPrompt(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
|
||||
}
|
||||
var body struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Language string `json:"language"`
|
||||
Prompts map[string]string `json:"prompts"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
prompts := body.Prompts
|
||||
if prompts == nil {
|
||||
prompts = map[string]string{}
|
||||
lang := strings.TrimSpace(body.Language)
|
||||
if lang == "" {
|
||||
lang = company.LoadLanguage(r.Context(), s.Pool, cid)
|
||||
}
|
||||
// Legacy single-prompt body: set/clear one language, preserve others.
|
||||
existing, gerr := s.Catalog.GetCategory(r.Context(), cid, id)
|
||||
if gerr == nil {
|
||||
if m, ok := existing["prompts"].(company.LangPromptMap); ok {
|
||||
for k, v := range m {
|
||||
prompts[k] = v
|
||||
}
|
||||
} else if raw, ok := existing["prompts"].(map[string]string); ok {
|
||||
for k, v := range raw {
|
||||
prompts[k] = v
|
||||
}
|
||||
} else if raw, ok := existing["prompts"].(map[string]any); ok {
|
||||
for k, v := range raw {
|
||||
if s, ok := v.(string); ok {
|
||||
prompts[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prompts[lang] = body.Prompt
|
||||
}
|
||||
item, err := s.Catalog.UpdateCategoryPrompt(r.Context(), cid, id, prompts)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update category prompt", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleListVariables(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
limit, offset := ParseLimitOffsetMax(r, maxTreePageLimit)
|
||||
page, total, err := s.Catalog.ListVariables(r.Context(), cid, catalog.ListFilter{Limit: limit, Offset: offset})
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]any{"variables": page, "total": total, "limit": limit, "offset": offset})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateVariable(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
value := body.Value
|
||||
if value == "" && body.Label != "" {
|
||||
value = body.Label
|
||||
}
|
||||
item, err := s.Catalog.CreateVariable(r.Context(), cid, body.Name, value, body.Description)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not create variable", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteVariable(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
|
||||
}
|
||||
if err := s.Catalog.DeleteVariable(r.Context(), cid, id); err != nil {
|
||||
if errors.Is(err, catalog.ErrNotFound) {
|
||||
Error(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
ClientOrLog(w, http.StatusNotFound, "could not delete variable", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleListAttributes(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
limit, offset := ParseLimitOffset(r)
|
||||
f := catalog.ListFilter{
|
||||
Query: QuerySearch(r),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
RootsOnly: r.URL.Query().Get("roots") == "1",
|
||||
ParentKey: r.URL.Query().Get("parent_key"),
|
||||
}
|
||||
items, total, err := s.Catalog.ListAttributes(r.Context(), cid, f)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]any{"attributes": items, "total": total, "limit": limit, "offset": offset})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateAttribute(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
var body struct {
|
||||
AttributeKey string `json:"attribute_key"`
|
||||
Name string `json:"name"`
|
||||
ValueType string `json:"value_type"`
|
||||
Unit *string `json:"unit"`
|
||||
Example *string `json:"example"`
|
||||
ParentKey *string `json:"parent_key"`
|
||||
}
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.CreateAttribute(r.Context(), cid, body.AttributeKey, body.Name, body.ValueType, body.Unit, body.Example, body.ParentKey)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not create attribute", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateAttribute(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
|
||||
}
|
||||
var body map[string]any
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.UpdateAttribute(r.Context(), cid, id, body)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update attribute", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteAttribute(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 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
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleListProducts(w http.ResponseWriter, r *http.Request) {
|
||||
cid, _ := CompanyIDFromContext(r.Context())
|
||||
limit, offset := ParseLimitOffset(r)
|
||||
cursor := strings.TrimSpace(r.URL.Query().Get("cursor"))
|
||||
afterID := strings.TrimSpace(firstNonEmpty(r.URL.Query().Get("after_id"), r.URL.Query().Get("afterId")))
|
||||
f := catalog.ListFilter{
|
||||
Query: QuerySearch(r),
|
||||
Status: r.URL.Query().Get("status"),
|
||||
Category: r.URL.Query().Get("category"),
|
||||
FeedID: firstNonEmpty(r.URL.Query().Get("feed_id"), r.URL.Query().Get("feedId")),
|
||||
Coverage: firstNonEmpty(r.URL.Query().Get("coverage"), r.URL.Query().Get("missing")),
|
||||
Eprel: firstNonEmpty(r.URL.Query().Get("eprel"), r.URL.Query().Get("has_eprel")),
|
||||
SyncChange: firstNonEmpty(r.URL.Query().Get("sync_change"), r.URL.Query().Get("syncChange"), r.URL.Query().Get("feed_change")),
|
||||
SortBy: firstNonEmpty(r.URL.Query().Get("sort_by"), r.URL.Query().Get("sortBy")),
|
||||
SortOrder: firstNonEmpty(r.URL.Query().Get("sort_order"), r.URL.Query().Get("sortOrder")),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Cursor: cursor,
|
||||
AfterID: afterID,
|
||||
}
|
||||
if catalog.HasProductCursor(f) {
|
||||
offset = 0
|
||||
}
|
||||
kind := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("kind")))
|
||||
// UI and some clients send kind=unprocessed for the raw inventory tab.
|
||||
if kind == "raw" || kind == "unprocessed" {
|
||||
items, total, err := s.Catalog.ListRawProducts(r.Context(), cid, f)
|
||||
if err != nil {
|
||||
if msg, ok := catalog.ClientError(err); ok {
|
||||
Error(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
resp := map[string]any{"products": items, "total": total, "kind": "raw", "limit": limit, "offset": offset}
|
||||
if nextCursor, nextAfter := catalog.NextProductCursor(f, items, limit); nextCursor != "" || nextAfter != "" {
|
||||
if nextCursor != "" {
|
||||
resp["next_cursor"] = nextCursor
|
||||
}
|
||||
if nextAfter != "" {
|
||||
resp["next_after_id"] = nextAfter
|
||||
}
|
||||
}
|
||||
JSON(w, http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
detailed := QueryDetailed(r)
|
||||
var (
|
||||
items []map[string]any
|
||||
total int64
|
||||
err error
|
||||
)
|
||||
if detailed {
|
||||
items, total, err = s.Catalog.ListProcessedProductsDetailed(r.Context(), cid, f)
|
||||
} else {
|
||||
items, total, err = s.Catalog.ListProcessedProducts(r.Context(), cid, f)
|
||||
}
|
||||
if err != nil {
|
||||
if msg, ok := catalog.ClientError(err); ok {
|
||||
Error(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
if detailed {
|
||||
attachProductQuality(items)
|
||||
}
|
||||
resp := map[string]any{"products": items, "total": total, "kind": "processed", "limit": limit, "offset": offset, "detailed": detailed}
|
||||
if nextCursor, nextAfter := catalog.NextProductCursor(f, items, limit); nextCursor != "" || nextAfter != "" {
|
||||
if nextCursor != "" {
|
||||
resp["next_cursor"] = nextCursor
|
||||
}
|
||||
if nextAfter != "" {
|
||||
resp["next_after_id"] = nextAfter
|
||||
}
|
||||
}
|
||||
JSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetProduct(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.GetProcessedProduct(r.Context(), cid, id)
|
||||
if err != nil {
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
Error(w, http.StatusInternalServerError, "lookup failed")
|
||||
return
|
||||
}
|
||||
item, err = s.Catalog.GetRawProduct(r.Context(), cid, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
Error(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "lookup failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateProduct(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
|
||||
}
|
||||
var body map[string]any
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
item, err := s.Catalog.UpdateProcessedProduct(r.Context(), cid, id, body)
|
||||
if err != nil {
|
||||
ClientOrLog(w, http.StatusBadRequest, "could not update product", err, catalog.ClientError)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, item)
|
||||
}
|
||||
Reference in New Issue
Block a user