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,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
|
||||
}
|
||||
Reference in New Issue
Block a user