package store import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // MetadataHeroSettingsKey is the app_settings row backing the household-wide metadata // hero composition. const MetadataHeroSettingsKey = "metadata_hero_settings" const ( MetadataHeroTitle = "title" MetadataHeroRatings = "ratings" MetadataHeroFacts = "facts" MetadataHeroSummary = "summary" MetadataHeroReason = "recommendation_reason" MetadataHeroTime = "time_remaining" ) var DefaultMetadataHeroContentOrder = []string{ MetadataHeroTitle, MetadataHeroRatings, MetadataHeroFacts, MetadataHeroSummary, } var MetadataHeroContentOptions = []string{ MetadataHeroTitle, MetadataHeroRatings, MetadataHeroFacts, MetadataHeroSummary, MetadataHeroReason, MetadataHeroTime, } // MetadataHeroSettings is one presentation decision for the whole household. It lives // outside user preferences because the hero is part of the product's shared composition, // not something that should change when the viewer changes. type MetadataHeroSettings struct { ContentOrder []string `json:"contentOrder"` UpdatedAt time.Time `json:"updatedAt"` UpdatedBy string `json:"updatedBy,omitempty"` } func NormalizeMetadataHeroSettings(settings MetadataHeroSettings) MetadataHeroSettings { seen := map[string]bool{} order := make([]string, 0, len(MetadataHeroContentOptions)) for _, entry := range settings.ContentOrder { if seen[entry] || !containsString(MetadataHeroContentOptions, entry) { continue } seen[entry] = true order = append(order, entry) } if len(order) == 0 { order = append([]string(nil), DefaultMetadataHeroContentOrder...) } settings.ContentOrder = order return settings } func (s *Store) MetadataHeroSettings(ctx context.Context) (MetadataHeroSettings, error) { var raw []byte err := s.pool.QueryRow(ctx, `SELECT value FROM app_settings WHERE key = $1`, MetadataHeroSettingsKey).Scan(&raw) if errors.Is(err, pgx.ErrNoRows) { return NormalizeMetadataHeroSettings(MetadataHeroSettings{}), nil } if err != nil { return MetadataHeroSettings{}, fmt.Errorf("store: read metadata hero settings: %w", err) } var settings MetadataHeroSettings if err := json.Unmarshal(raw, &settings); err != nil { return MetadataHeroSettings{}, fmt.Errorf("store: decode metadata hero settings: %w", err) } return NormalizeMetadataHeroSettings(settings), nil } func (s *Store) SetMetadataHeroSettings( ctx context.Context, settings MetadataHeroSettings, ) (MetadataHeroSettings, error) { settings = NormalizeMetadataHeroSettings(settings) settings.UpdatedAt = time.Now().UTC() raw, err := json.Marshal(settings) if err != nil { return MetadataHeroSettings{}, err } _, err = s.pool.Exec(ctx, ` INSERT INTO app_settings (key, value, updated_at) VALUES ($1, $2::jsonb, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, MetadataHeroSettingsKey, string(raw)) if err != nil { return MetadataHeroSettings{}, fmt.Errorf("store: write metadata hero settings: %w", err) } return settings, nil }