Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
180 lines
5.6 KiB
Go
180 lines
5.6 KiB
Go
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
|
|
}
|