483 lines
15 KiB
Go
483 lines
15 KiB
Go
package catalog
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
const standardFieldSelect = `
|
||
|
|
sf.id, sf.company_id, sf.name, sf.key, sf.type, sf.group_id,
|
||
|
|
sf.is_required, sf.description, sf.default_value, sf.is_system,
|
||
|
|
sf.enabled, sf.unit, sf.sort_order, sf.mapping_hints,
|
||
|
|
sf.created_at, sf.updated_at, fg.name AS group_name`
|
||
|
|
|
||
|
|
var standardFieldCols = []string{
|
||
|
|
"id", "company_id", "name", "key", "type", "group_id",
|
||
|
|
"is_required", "description", "default_value", "is_system",
|
||
|
|
"enabled", "unit", "sort_order", "mapping_hints",
|
||
|
|
"created_at", "updated_at", "group_name",
|
||
|
|
}
|
||
|
|
|
||
|
|
// Allowed standard-field types (Shopify-aligned product field kinds).
|
||
|
|
var standardFieldTypes = map[string]struct{}{
|
||
|
|
"string": {}, "number": {}, "boolean": {}, "date": {},
|
||
|
|
"url": {}, "image": {}, "dimension": {}, "weight": {},
|
||
|
|
"color": {}, "custom": {},
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateStandardFieldType(typ string) error {
|
||
|
|
typ = strings.TrimSpace(typ)
|
||
|
|
if typ == "" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if _, ok := standardFieldTypes[typ]; !ok {
|
||
|
|
return ClientMsg("type must be one of: string, number, boolean, date, url, image, dimension, weight, color, custom")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) ListFieldGroups(ctx context.Context, companyID uuid.UUID) ([]map[string]any, error) {
|
||
|
|
rows, err := s.Pool.Query(ctx, `
|
||
|
|
SELECT id, company_id, name, description, "order", is_system, created_at, updated_at
|
||
|
|
FROM field_groups
|
||
|
|
WHERE company_id = $1
|
||
|
|
ORDER BY "order" ASC, name ASC`, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
return scanMaps(rows, []string{"id", "company_id", "name", "description", "order", "is_system", "created_at", "updated_at"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) GetFieldGroup(ctx context.Context, companyID, id uuid.UUID) (map[string]any, error) {
|
||
|
|
row := s.Pool.QueryRow(ctx, `
|
||
|
|
SELECT id, company_id, name, description, "order", is_system, created_at, updated_at
|
||
|
|
FROM field_groups WHERE id = $1 AND company_id = $2`, id, companyID)
|
||
|
|
item, err := scanMap(row, []string{"id", "company_id", "name", "description", "order", "is_system", "created_at", "updated_at"})
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CreateFieldGroup(ctx context.Context, companyID uuid.UUID, name string, description *string, order int) (map[string]any, error) {
|
||
|
|
name = strings.TrimSpace(name)
|
||
|
|
if name == "" {
|
||
|
|
return nil, ClientMsg("name required")
|
||
|
|
}
|
||
|
|
var id uuid.UUID
|
||
|
|
err := s.Pool.QueryRow(ctx, `
|
||
|
|
INSERT INTO field_groups (company_id, name, description, "order")
|
||
|
|
VALUES ($1, $2, $3, $4) RETURNING id`, companyID, name, description, order).Scan(&id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.GetFieldGroup(ctx, companyID, id)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) UpdateFieldGroup(ctx context.Context, companyID, id uuid.UUID, body map[string]any) (map[string]any, error) {
|
||
|
|
existing, err := s.GetFieldGroup(ctx, companyID, id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if isTruthy(existing["is_system"]) {
|
||
|
|
return nil, ErrSystemImmutable
|
||
|
|
}
|
||
|
|
name := pickString(body, "name")
|
||
|
|
desc := pickStringPtr(body, "description")
|
||
|
|
order, hasOrder := pickInt(body, "order")
|
||
|
|
_, err = s.Pool.Exec(ctx, `
|
||
|
|
UPDATE field_groups SET
|
||
|
|
name = CASE WHEN $3 <> '' THEN $3 ELSE name END,
|
||
|
|
description = CASE WHEN $4::text IS NOT NULL THEN $4 ELSE description END,
|
||
|
|
"order" = CASE WHEN $5::boolean THEN $6 ELSE "order" END,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1 AND company_id = $2`,
|
||
|
|
id, companyID, name, desc, hasOrder, order)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.GetFieldGroup(ctx, companyID, id)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) DeleteFieldGroup(ctx context.Context, companyID, id uuid.UUID) error {
|
||
|
|
existing, err := s.GetFieldGroup(ctx, companyID, id)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if isTruthy(existing["is_system"]) {
|
||
|
|
return ErrSystemImmutable
|
||
|
|
}
|
||
|
|
ct, err := s.Pool.Exec(ctx, `DELETE FROM field_groups WHERE id = $1 AND company_id = $2`, id, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if ct.RowsAffected() == 0 {
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) ListStandardFields(ctx context.Context, companyID uuid.UUID, enabledOnly bool) ([]map[string]any, error) {
|
||
|
|
q := `
|
||
|
|
SELECT ` + standardFieldSelect + `
|
||
|
|
FROM standard_fields sf
|
||
|
|
LEFT JOIN field_groups fg ON fg.id = sf.group_id
|
||
|
|
WHERE sf.company_id = $1`
|
||
|
|
if enabledOnly {
|
||
|
|
q += ` AND sf.enabled = true`
|
||
|
|
}
|
||
|
|
q += ` ORDER BY COALESCE(fg."order", 0), sf.sort_order ASC, sf.name ASC`
|
||
|
|
rows, err := s.Pool.Query(ctx, q, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
return scanMaps(rows, standardFieldCols)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) GetStandardField(ctx context.Context, companyID, id uuid.UUID) (map[string]any, error) {
|
||
|
|
row := s.Pool.QueryRow(ctx, `
|
||
|
|
SELECT `+standardFieldSelect+`
|
||
|
|
FROM standard_fields sf
|
||
|
|
LEFT JOIN field_groups fg ON fg.id = sf.group_id
|
||
|
|
WHERE sf.id = $1 AND sf.company_id = $2`, id, companyID)
|
||
|
|
item, err := scanMap(row, standardFieldCols)
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CreateStandardField(ctx context.Context, companyID uuid.UUID, body map[string]any) (map[string]any, error) {
|
||
|
|
name := strings.TrimSpace(pickString(body, "name"))
|
||
|
|
key := strings.TrimSpace(pickString(body, "key"))
|
||
|
|
typ := strings.TrimSpace(pickString(body, "type"))
|
||
|
|
groupIDStr := strings.TrimSpace(pickString(body, "group_id", "groupId"))
|
||
|
|
if name == "" || key == "" || typ == "" || groupIDStr == "" {
|
||
|
|
return nil, ClientMsg("name, key, type, and group_id required")
|
||
|
|
}
|
||
|
|
if err := validateStandardFieldType(typ); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
groupID, err := uuid.Parse(groupIDStr)
|
||
|
|
if err != nil {
|
||
|
|
return nil, ClientMsg("invalid group_id")
|
||
|
|
}
|
||
|
|
if _, err := s.GetFieldGroup(ctx, companyID, groupID); err != nil {
|
||
|
|
return nil, ClientMsg("group not found")
|
||
|
|
}
|
||
|
|
isRequired := pickBool(body, "is_required", "isRequired")
|
||
|
|
enabled := true
|
||
|
|
if v, ok := pickBoolOk(body, "enabled"); ok {
|
||
|
|
enabled = v
|
||
|
|
}
|
||
|
|
desc := pickStringPtr(body, "description")
|
||
|
|
defVal := pickStringPtr(body, "default_value", "defaultValue")
|
||
|
|
unit := pickStringPtr(body, "unit")
|
||
|
|
sortOrder, _ := pickInt(body, "sort_order", "sortOrder")
|
||
|
|
hintsJSON, err := marshalMappingHints(body)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
var id uuid.UUID
|
||
|
|
err = s.Pool.QueryRow(ctx, `
|
||
|
|
INSERT INTO standard_fields (
|
||
|
|
company_id, name, key, type, group_id, is_required, description, default_value,
|
||
|
|
enabled, unit, sort_order, mapping_hints
|
||
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12::jsonb) RETURNING id`,
|
||
|
|
companyID, name, key, typ, groupID, isRequired, desc, defVal,
|
||
|
|
enabled, unit, sortOrder, hintsJSON).Scan(&id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.GetStandardField(ctx, companyID, id)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) UpdateStandardField(ctx context.Context, companyID, id uuid.UUID, body map[string]any) (map[string]any, error) {
|
||
|
|
existing, err := s.GetStandardField(ctx, companyID, id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
system := isTruthy(existing["is_system"])
|
||
|
|
|
||
|
|
// System fields keep identity immutable; config (enabled/unit/defaults/hints) stays editable.
|
||
|
|
name := ""
|
||
|
|
key := ""
|
||
|
|
typ := ""
|
||
|
|
var groupID *uuid.UUID
|
||
|
|
if !system {
|
||
|
|
name = strings.TrimSpace(pickString(body, "name"))
|
||
|
|
key = strings.TrimSpace(pickString(body, "key"))
|
||
|
|
typ = strings.TrimSpace(pickString(body, "type"))
|
||
|
|
if err := validateStandardFieldType(typ); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
groupIDStr := strings.TrimSpace(pickString(body, "group_id", "groupId"))
|
||
|
|
if groupIDStr != "" {
|
||
|
|
parsed, err := uuid.Parse(groupIDStr)
|
||
|
|
if err != nil {
|
||
|
|
return nil, ClientMsg("invalid group_id")
|
||
|
|
}
|
||
|
|
if _, err := s.GetFieldGroup(ctx, companyID, parsed); err != nil {
|
||
|
|
return nil, ClientMsg("group not found")
|
||
|
|
}
|
||
|
|
groupID = &parsed
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
isRequired, hasRequired := pickBoolOk(body, "is_required", "isRequired")
|
||
|
|
enabled, hasEnabled := pickBoolOk(body, "enabled")
|
||
|
|
desc := pickStringPtr(body, "description")
|
||
|
|
defVal := pickStringPtr(body, "default_value", "defaultValue")
|
||
|
|
unit := pickStringPtr(body, "unit")
|
||
|
|
sortOrder, hasSort := pickInt(body, "sort_order", "sortOrder")
|
||
|
|
hintsJSON, hasHints, err := marshalMappingHintsOk(body)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err = s.Pool.Exec(ctx, `
|
||
|
|
UPDATE standard_fields SET
|
||
|
|
name = CASE WHEN $3 <> '' THEN $3 ELSE name END,
|
||
|
|
key = CASE WHEN $4 <> '' THEN $4 ELSE key END,
|
||
|
|
type = CASE WHEN $5 <> '' THEN $5 ELSE type END,
|
||
|
|
group_id = COALESCE($6, group_id),
|
||
|
|
is_required = CASE WHEN $7::boolean THEN $8 ELSE is_required END,
|
||
|
|
description = CASE WHEN $9::text IS NOT NULL THEN $9 ELSE description END,
|
||
|
|
default_value = CASE WHEN $10::text IS NOT NULL THEN $10 ELSE default_value END,
|
||
|
|
enabled = CASE WHEN $11::boolean THEN $12 ELSE enabled END,
|
||
|
|
unit = CASE WHEN $13::text IS NOT NULL THEN $13 ELSE unit END,
|
||
|
|
sort_order = CASE WHEN $14::boolean THEN $15 ELSE sort_order END,
|
||
|
|
mapping_hints = CASE WHEN $16::boolean THEN $17::jsonb ELSE mapping_hints END,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1 AND company_id = $2`,
|
||
|
|
id, companyID, name, key, typ, groupID,
|
||
|
|
hasRequired, isRequired, desc, defVal,
|
||
|
|
hasEnabled, enabled, unit, hasSort, sortOrder,
|
||
|
|
hasHints, hintsJSON)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.GetStandardField(ctx, companyID, id)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) DeleteStandardField(ctx context.Context, companyID, id uuid.UUID) error {
|
||
|
|
existing, err := s.GetStandardField(ctx, companyID, id)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if isTruthy(existing["is_system"]) {
|
||
|
|
return ErrSystemImmutable
|
||
|
|
}
|
||
|
|
ct, err := s.Pool.Exec(ctx, `DELETE FROM standard_fields WHERE id = $1 AND company_id = $2`, id, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if ct.RowsAffected() == 0 {
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// BulkSetStandardFieldsEnabled toggles enabled for the given field IDs (any fields, including system).
|
||
|
|
func (s *Service) BulkSetStandardFieldsEnabled(ctx context.Context, companyID uuid.UUID, ids []uuid.UUID, enabled bool) (int64, error) {
|
||
|
|
if len(ids) == 0 {
|
||
|
|
return 0, ClientMsg("ids required")
|
||
|
|
}
|
||
|
|
ct, err := s.Pool.Exec(ctx, `
|
||
|
|
UPDATE standard_fields SET enabled = $3, updated_at = now()
|
||
|
|
WHERE company_id = $1 AND id = ANY($2::uuid[])`, companyID, ids, enabled)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
return ct.RowsAffected(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// EnableRecommendedEcommerce ensures the ecommerce catalog exists and enables the recommended set.
|
||
|
|
func (s *Service) EnableRecommendedEcommerce(ctx context.Context, companyID uuid.UUID) ([]map[string]any, error) {
|
||
|
|
if err := s.EnsureEcommerceCatalog(ctx, companyID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
keys := recommendedEcommerceKeys()
|
||
|
|
_, err := s.Pool.Exec(ctx, `
|
||
|
|
UPDATE standard_fields SET enabled = true, updated_at = now()
|
||
|
|
WHERE company_id = $1 AND key = ANY($2::text[])`, companyID, keys)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
// Also enable every catalog field that ships Enabled:true in the ecommerce seed
|
||
|
|
// so mapping/forms see the full legacy-compatible set after migration gaps.
|
||
|
|
_, err = s.Pool.Exec(ctx, `
|
||
|
|
UPDATE standard_fields SET enabled = true, updated_at = now()
|
||
|
|
WHERE company_id = $1 AND is_system = true AND enabled = false`, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.ListStandardFields(ctx, companyID, false)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) ListStructuredDescriptions(ctx context.Context, companyID uuid.UUID) ([]map[string]any, error) {
|
||
|
|
rows, err := s.Pool.Query(ctx, `
|
||
|
|
SELECT id, company_id, field_key, type, created_at, updated_at
|
||
|
|
FROM structured_description_fields
|
||
|
|
WHERE company_id = $1
|
||
|
|
ORDER BY field_key`, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
return scanMaps(rows, []string{"id", "company_id", "field_key", "type", "created_at", "updated_at"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) GetStructuredDescription(ctx context.Context, companyID, id uuid.UUID) (map[string]any, error) {
|
||
|
|
row := s.Pool.QueryRow(ctx, `
|
||
|
|
SELECT id, company_id, field_key, type, created_at, updated_at
|
||
|
|
FROM structured_description_fields
|
||
|
|
WHERE id = $1 AND company_id = $2`, id, companyID)
|
||
|
|
item, err := scanMap(row, []string{"id", "company_id", "field_key", "type", "created_at", "updated_at"})
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return nil, ErrNotFound
|
||
|
|
}
|
||
|
|
return item, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CreateStructuredDescription(ctx context.Context, companyID uuid.UUID, fieldKey, typ string) (map[string]any, error) {
|
||
|
|
fieldKey = strings.TrimSpace(fieldKey)
|
||
|
|
typ = strings.TrimSpace(typ)
|
||
|
|
if fieldKey == "" {
|
||
|
|
return nil, ClientMsg("field_key required")
|
||
|
|
}
|
||
|
|
if typ == "" {
|
||
|
|
typ = "text"
|
||
|
|
}
|
||
|
|
var id uuid.UUID
|
||
|
|
err := s.Pool.QueryRow(ctx, `
|
||
|
|
INSERT INTO structured_description_fields (company_id, field_key, type)
|
||
|
|
VALUES ($1, $2, $3) RETURNING id`, companyID, fieldKey, typ).Scan(&id)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.GetStructuredDescription(ctx, companyID, id)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) DeleteStructuredDescription(ctx context.Context, companyID, id uuid.UUID) error {
|
||
|
|
ct, err := s.Pool.Exec(ctx, `
|
||
|
|
DELETE FROM structured_description_fields WHERE id = $1 AND company_id = $2`, id, companyID)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if ct.RowsAffected() == 0 {
|
||
|
|
return ErrNotFound
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func marshalMappingHints(body map[string]any) (string, error) {
|
||
|
|
s, ok, err := marshalMappingHintsOk(body)
|
||
|
|
if err != nil {
|
||
|
|
return "[]", err
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
return "[]", nil
|
||
|
|
}
|
||
|
|
return s, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func marshalMappingHintsOk(body map[string]any) (string, bool, error) {
|
||
|
|
raw, ok := body["mapping_hints"]
|
||
|
|
if !ok {
|
||
|
|
raw, ok = body["mappingHints"]
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
return "[]", false, nil
|
||
|
|
}
|
||
|
|
switch t := raw.(type) {
|
||
|
|
case nil:
|
||
|
|
return "[]", true, nil
|
||
|
|
case string:
|
||
|
|
if strings.TrimSpace(t) == "" {
|
||
|
|
return "[]", true, nil
|
||
|
|
}
|
||
|
|
var probe any
|
||
|
|
if err := json.Unmarshal([]byte(t), &probe); err != nil {
|
||
|
|
return "", false, ClientMsg("mapping_hints must be JSON array")
|
||
|
|
}
|
||
|
|
return t, true, nil
|
||
|
|
default:
|
||
|
|
b, err := json.Marshal(t)
|
||
|
|
if err != nil {
|
||
|
|
return "", false, ClientMsg("invalid mapping_hints")
|
||
|
|
}
|
||
|
|
return string(b), true, nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickString(m map[string]any, keys ...string) string {
|
||
|
|
for _, k := range keys {
|
||
|
|
if v, ok := m[k]; ok && v != nil {
|
||
|
|
if s, ok := v.(string); ok {
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickStringPtr(m map[string]any, keys ...string) *string {
|
||
|
|
for _, k := range keys {
|
||
|
|
if v, ok := m[k]; ok {
|
||
|
|
if v == nil {
|
||
|
|
empty := ""
|
||
|
|
return &empty
|
||
|
|
}
|
||
|
|
if s, ok := v.(string); ok {
|
||
|
|
return &s
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickBool(m map[string]any, keys ...string) bool {
|
||
|
|
b, _ := pickBoolOk(m, keys...)
|
||
|
|
return b
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickBoolOk(m map[string]any, keys ...string) (bool, bool) {
|
||
|
|
for _, k := range keys {
|
||
|
|
if v, ok := m[k]; ok {
|
||
|
|
if b, ok := v.(bool); ok {
|
||
|
|
return b, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false, false
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickInt(m map[string]any, keys ...string) (int, bool) {
|
||
|
|
for _, k := range keys {
|
||
|
|
if v, ok := m[k]; ok && v != nil {
|
||
|
|
switch n := v.(type) {
|
||
|
|
case float64:
|
||
|
|
return int(n), true
|
||
|
|
case int:
|
||
|
|
return n, true
|
||
|
|
case int64:
|
||
|
|
return int(n), true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
|
||
|
|
func isTruthy(v any) bool {
|
||
|
|
b, ok := v.(bool)
|
||
|
|
return ok && b
|
||
|
|
}
|