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:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
package catalog
import (
"context"
"errors"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
func (s *Service) ListCategoryAttributes(ctx context.Context, companyID uuid.UUID, categoryUniqueID string) ([]map[string]any, error) {
categoryUniqueID = strings.TrimSpace(categoryUniqueID)
if categoryUniqueID == "" {
return nil, ClientMsg("category_unique_id required")
}
rows, err := s.Pool.Query(ctx, `
SELECT ca.id, ca.category_unique_id, ca.attribute_id, ca.required,
a.attribute_key, a.name, a.value_type
FROM category_attributes ca
INNER JOIN attributes a ON a.id = ca.attribute_id AND a.company_id = ca.company_id
WHERE ca.company_id = $1 AND ca.category_unique_id = $2
ORDER BY a.name`, companyID, categoryUniqueID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanMaps(rows, []string{"id", "category_unique_id", "attribute_id", "required", "attribute_key", "name", "value_type"})
}
func (s *Service) LinkCategoryAttribute(ctx context.Context, companyID uuid.UUID, categoryUniqueID string, attributeID uuid.UUID, required bool) (map[string]any, error) {
categoryUniqueID = strings.TrimSpace(categoryUniqueID)
if categoryUniqueID == "" {
return nil, ClientMsg("category_unique_id required")
}
var catExists bool
if err := s.Pool.QueryRow(ctx, `
SELECT EXISTS(SELECT 1 FROM categories WHERE company_id = $1 AND unique_id = $2)`,
companyID, categoryUniqueID).Scan(&catExists); err != nil {
return nil, err
}
if !catExists {
return nil, ClientMsg("category not found")
}
var attrCompany uuid.UUID
err := s.Pool.QueryRow(ctx, `SELECT company_id FROM attributes WHERE id = $1`, attributeID).Scan(&attrCompany)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ClientMsg("attribute not found")
}
return nil, err
}
if attrCompany != companyID {
return nil, ClientMsg("attribute not found")
}
var id uuid.UUID
err = s.Pool.QueryRow(ctx, `
INSERT INTO category_attributes (company_id, category_unique_id, attribute_id, required)
VALUES ($1, $2, $3, $4)
ON CONFLICT (company_id, category_unique_id, attribute_id)
DO UPDATE SET required = EXCLUDED.required, updated_at = now()
RETURNING id`, companyID, categoryUniqueID, attributeID, required).Scan(&id)
if err != nil {
return nil, err
}
row := s.Pool.QueryRow(ctx, `
SELECT ca.id, ca.category_unique_id, ca.attribute_id, ca.required,
a.attribute_key, a.name, a.value_type
FROM category_attributes ca
INNER JOIN attributes a ON a.id = ca.attribute_id
WHERE ca.id = $1 AND ca.company_id = $2`, id, companyID)
return scanMap(row, []string{"id", "category_unique_id", "attribute_id", "required", "attribute_key", "name", "value_type"})
}
func (s *Service) UnlinkCategoryAttribute(ctx context.Context, companyID uuid.UUID, categoryUniqueID string, attributeID uuid.UUID) error {
categoryUniqueID = strings.TrimSpace(categoryUniqueID)
ct, err := s.Pool.Exec(ctx, `
DELETE FROM category_attributes
WHERE company_id = $1 AND category_unique_id = $2 AND attribute_id = $3`,
companyID, categoryUniqueID, attributeID)
if err != nil {
return err
}
if ct.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
func (s *Service) ReplaceCategoryAttributes(ctx context.Context, companyID uuid.UUID, categoryUniqueID string, attributeIDs []uuid.UUID, required map[string]bool) error {
categoryUniqueID = strings.TrimSpace(categoryUniqueID)
if categoryUniqueID == "" {
return ClientMsg("category_unique_id required")
}
tx, err := s.Pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var catExists bool
if err := tx.QueryRow(ctx, `
SELECT EXISTS(SELECT 1 FROM categories WHERE company_id = $1 AND unique_id = $2)`,
companyID, categoryUniqueID).Scan(&catExists); err != nil {
return err
}
if !catExists {
return ClientMsg("category not found")
}
if _, err := tx.Exec(ctx, `
DELETE FROM category_attributes WHERE company_id = $1 AND category_unique_id = $2`,
companyID, categoryUniqueID); err != nil {
return err
}
if len(attributeIDs) == 0 {
return tx.Commit(ctx)
}
if required == nil {
required = map[string]bool{}
}
ownedRows, err := tx.Query(ctx, `
SELECT id FROM attributes WHERE company_id = $1 AND id = ANY($2::uuid[])`,
companyID, attributeIDs)
if err != nil {
return err
}
owned := make([]uuid.UUID, 0, len(attributeIDs))
for ownedRows.Next() {
var id uuid.UUID
if err := ownedRows.Scan(&id); err != nil {
ownedRows.Close()
return err
}
owned = append(owned, id)
}
err = ownedRows.Err()
ownedRows.Close()
if err != nil {
return err
}
if err := validateAttributeIDsOwned(attributeIDs, owned); err != nil {
return err
}
reqs := make([]bool, len(attributeIDs))
for i, aid := range attributeIDs {
reqs[i] = required[aid.String()]
}
if _, err := tx.Exec(ctx, `
INSERT INTO category_attributes (company_id, category_unique_id, attribute_id, required)
SELECT $1, $2, u.attribute_id, u.required
FROM unnest($3::uuid[], $4::boolean[]) AS u(attribute_id, required)`,
companyID, categoryUniqueID, attributeIDs, reqs); err != nil {
return err
}
return tx.Commit(ctx)
}
// validateAttributeIDsOwned ensures every requested attribute ID is present in the
// company-scoped ownership query result. Missing or cross-tenant IDs surface as
// "attribute not found" (same message as the former per-row SELECT path).
func validateAttributeIDsOwned(attributeIDs, owned []uuid.UUID) error {
if len(attributeIDs) == 0 {
return nil
}
set := make(map[uuid.UUID]struct{}, len(owned))
for _, id := range owned {
set[id] = struct{}{}
}
for _, aid := range attributeIDs {
if _, ok := set[aid]; !ok {
return ClientMsg("attribute not found")
}
}
return nil
}